From 105f48a1269594859e784b04ee59e94d19eff8d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Conejo?= Date: Tue, 16 Jun 2026 17:07:15 +0200 Subject: [PATCH] feat(provider): add Manifest as a built-in provider Closes #32572 Manifest is a model router for AI agents that exposes an OpenAI-compatible endpoint. This adds first-class support so users can `/connect manifest` instead of hand-editing opencode.json. - Core plugin: X-Source attribution header - Auth plugin: API key authentication - Custom loader with /v1/models discovery - Provider docs in providers.mdx - 2 contract tests --- packages/core/src/plugin/provider.ts | 2 + packages/core/src/plugin/provider/manifest.ts | 20 +++ packages/core/src/provider.ts | 1 + packages/opencode/src/plugin/index.ts | 2 + packages/opencode/src/plugin/manifest.ts | 15 +++ packages/opencode/src/provider/provider.ts | 73 +++++++++++ .../opencode/test/provider/manifest.test.ts | 121 ++++++++++++++++++ packages/web/src/content/docs/providers.mdx | 55 ++++++++ 8 files changed, 289 insertions(+) create mode 100644 packages/core/src/plugin/provider/manifest.ts create mode 100644 packages/opencode/src/plugin/manifest.ts create mode 100644 packages/opencode/test/provider/manifest.test.ts diff --git a/packages/core/src/plugin/provider.ts b/packages/core/src/plugin/provider.ts index ea3939b750de..859594134cb6 100644 --- a/packages/core/src/plugin/provider.ts +++ b/packages/core/src/plugin/provider.ts @@ -16,6 +16,7 @@ import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "./provider/goog import { GroqPlugin } from "./provider/groq" import { KiloPlugin } from "./provider/kilo" import { LLMGatewayPlugin } from "./provider/llmgateway" +import { ManifestPlugin } from "./provider/manifest" import { MistralPlugin } from "./provider/mistral" import { NvidiaPlugin } from "./provider/nvidia" import { OpenAIPlugin } from "./provider/openai" @@ -51,6 +52,7 @@ export const ProviderPlugins = [ GroqPlugin, KiloPlugin, LLMGatewayPlugin, + ManifestPlugin, MistralPlugin, NvidiaPlugin, OpencodePlugin, diff --git a/packages/core/src/plugin/provider/manifest.ts b/packages/core/src/plugin/provider/manifest.ts new file mode 100644 index 000000000000..0dcd488aa7ae --- /dev/null +++ b/packages/core/src/plugin/provider/manifest.ts @@ -0,0 +1,20 @@ +import { Effect } from "effect" +import { PluginV2 } from "../../plugin" + +export const ManifestPlugin = PluginV2.define({ + id: PluginV2.ID.make("manifest"), + effect: Effect.gen(function* () { + return { + "catalog.transform": Effect.fn(function* (evt) { + for (const item of evt.provider.list()) { + if (item.provider.id !== "manifest") continue + if (item.provider.api.type !== "aisdk") continue + if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue + evt.provider.update(item.provider.id, (provider) => { + provider.request.headers["X-Source"] = "opencode" + }) + } + }), + } + }), +}) diff --git a/packages/core/src/provider.ts b/packages/core/src/provider.ts index 3f5424a47f36..b06bef1bac21 100644 --- a/packages/core/src/provider.ts +++ b/packages/core/src/provider.ts @@ -16,6 +16,7 @@ export const ID = Schema.String.pipe( amazonBedrock: schema.make("amazon-bedrock"), azure: schema.make("azure"), openrouter: schema.make("openrouter"), + manifest: schema.make("manifest"), mistral: schema.make("mistral"), gitlab: schema.make("gitlab"), })), diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 0f71b39a9d5e..8e0646efdc1d 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -19,6 +19,7 @@ import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cl import { AzureAuthPlugin } from "./azure" import { DigitalOceanAuthPlugin } from "./digitalocean" import { XaiAuthPlugin } from "./xai" +import { ManifestAuthPlugin } from "./manifest" import { SnowflakeCortexAuthPlugin } from "./snowflake-cortex" import { Effect, Layer, Context } from "effect" import { EffectBridge } from "@/effect/bridge" @@ -76,6 +77,7 @@ function internalPlugins(flags: RuntimeFlags.Info): PluginInstance[] { CloudflareAIGatewayAuthPlugin, AzureAuthPlugin, DigitalOceanAuthPlugin, + ManifestAuthPlugin, SnowflakeCortexAuthPlugin, XaiAuthPlugin, ] diff --git a/packages/opencode/src/plugin/manifest.ts b/packages/opencode/src/plugin/manifest.ts new file mode 100644 index 000000000000..097160bcd127 --- /dev/null +++ b/packages/opencode/src/plugin/manifest.ts @@ -0,0 +1,15 @@ +import type { Hooks, PluginInput } from "@opencode-ai/plugin" + +export async function ManifestAuthPlugin(_input: PluginInput): Promise { + return { + auth: { + provider: "manifest", + methods: [ + { + type: "api", + label: "Manifest API Key", + }, + ], + }, + } +} diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 4352f8a9b519..d1452928f79a 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -451,6 +451,65 @@ function custom(dep: CustomDep): Record { }, }, }), + manifest: Effect.fnUntraced(function* (provider: Info) { + const config = yield* dep.config() + const baseURL = provider.options?.baseURL ?? config.provider?.["manifest"]?.options?.baseURL + + return { + autoload: false, + options: { + headers: { + "X-Source": "opencode", + }, + }, + async discoverModels() { + if (!baseURL) return {} + + const modelsURL = `${baseURL.replace(/\/+$/, "")}/models` + const apiKey = provider.options?.apiKey ?? config.provider?.["manifest"]?.options?.apiKey + const headers: Record = { Accept: "application/json" } + if (apiKey) headers["Authorization"] = `Bearer ${apiKey}` + + try { + const res = await fetch(modelsURL, { headers, signal: AbortSignal.timeout(5_000) }) + if (!res.ok) return {} + const body = (await res.json()) as { data?: Array<{ id: string; display_name?: string }> } + if (!body.data || !Array.isArray(body.data)) return {} + + const models: Record = {} + for (const item of body.data) { + if (!item.id || typeof item.id !== "string") continue + models[item.id] = { + id: ModelV2.ID.make(item.id), + providerID: ProviderV2.ID.make("manifest"), + name: item.display_name ?? item.id, + family: "manifest", + api: { id: item.id, url: baseURL, npm: "@ai-sdk/openai-compatible" }, + status: "active", + headers: {}, + options: {}, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: 1_000_000, output: 32_768 }, + capabilities: { + temperature: true, + reasoning: true, + attachment: false, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + release_date: "", + variants: {}, + } + } + return models + } catch { + return {} + } + }, + } + }), openrouter: () => Effect.succeed({ autoload: false, @@ -1557,6 +1616,20 @@ export const layer = Layer.effect( }) } + const manifest = ProviderV2.ID.make("manifest") + if (discoveryLoaders[manifest] && providers[manifest] && isProviderAllowed(manifest)) { + yield* Effect.promise(async () => { + try { + const discovered = await discoveryLoaders[manifest]() + for (const [modelID, model] of Object.entries(discovered)) { + if (!providers[manifest].models[modelID]) { + providers[manifest].models[modelID] = model + } + } + } catch (e) {} + }) + } + for (const [id, provider] of Object.entries(providers)) { const providerID = ProviderV2.ID.make(id) if (!isProviderAllowed(providerID)) { diff --git a/packages/opencode/test/provider/manifest.test.ts b/packages/opencode/test/provider/manifest.test.ts new file mode 100644 index 000000000000..a94039ed130a --- /dev/null +++ b/packages/opencode/test/provider/manifest.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, mock, beforeAll, afterAll } from "bun:test" +import { Provider } from "../../src/provider/provider" +import { Effect } from "effect" +import { testEffect } from "../lib/effect" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { createServer, type Server as HTTPServer } from "http" + +const MANIFEST = ProviderV2.ID.make("manifest") +const it = testEffect(Provider.defaultLayer) + +const withEnv = (values: Record, effect: Effect.Effect) => + Effect.acquireUseRelease( + Effect.sync(() => { + const previous = Object.fromEntries(Object.keys(values).map((key) => [key, process.env[key]] as const)) + Object.assign(process.env, values) + return previous + }), + () => effect, + (previous) => + Effect.sync(() => { + for (const [key, value] of Object.entries(previous)) { + if (value === undefined) delete process.env[key] + else process.env[key] = value + } + }), + ) + +describe("manifest provider", () => { + let server: HTTPServer + let port: number + + beforeAll(async () => { + server = createServer((req, res) => { + if (req.url === "/v1/models") { + res.writeHead(200, { "Content-Type": "application/json" }) + res.end( + JSON.stringify({ + object: "list", + data: [ + { + id: "auto", + object: "model", + type: "model", + display_name: "Manifest Auto", + }, + ], + has_more: false, + first_id: "auto", + last_id: "auto", + }), + ) + return + } + res.writeHead(404) + res.end() + }) + + await new Promise((resolve) => { + server.listen(0, "127.0.0.1", () => { + const addr = server.address() + if (addr && typeof addr === "object") { + port = addr.port + } + resolve() + }) + }) + }) + + afterAll(() => { + server?.close() + }) + + it.instance( + "manifest provider loads from config with model discovery", + () => + withEnv( + {}, + Effect.gen(function* () { + const provider = yield* Provider.Service + const providers = yield* provider.list() + expect(providers[MANIFEST]).toBeDefined() + expect(providers[MANIFEST].source).toBe("config") + expect(providers[MANIFEST].models["auto"]).toBeDefined() + expect(providers[MANIFEST].models["auto"].name).toBe("Manifest Auto") + expect(providers[MANIFEST].models["auto"].api.npm).toBe("@ai-sdk/openai-compatible") + }), + ), + { + config: { + provider: { + manifest: { + npm: "@ai-sdk/openai-compatible", + name: "Manifest", + options: { + get baseURL() { + return `http://127.0.0.1:${port}/v1` + }, + apiKey: "mnfst_test_key", + }, + models: { + auto: { + name: "Manifest Auto", + }, + }, + }, + }, + }, + }, + ) + + it.instance( + "manifest provider not loaded without config", + () => + Effect.gen(function* () { + const provider = yield* Provider.Service + const providers = yield* provider.list() + expect(providers[MANIFEST]).toBeUndefined() + }), + { config: {} }, + ) +}) diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index b31ebb67792c..4e73441bafd4 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -1796,6 +1796,61 @@ OpenCode Zen is a list of tested and verified models provided by the OpenCode te --- +### Manifest + +[Manifest](https://manifest.build) is a model router for AI agents. It sits between your agent and LLM providers, scores each request by complexity, and routes it to the best model that can handle it. Manifest tracks costs, tokens, and messages across all your agents. + +1. Get your Manifest API key from the Manifest dashboard. Keys use the `mnfst_` prefix. + +2. Add Manifest to your `opencode.json` configuration: + + ```json title="opencode.json" + { + "$schema": "https://opencode.ai/config.json", + "provider": { + "manifest": { + "npm": "@ai-sdk/openai-compatible", + "name": "Manifest", + "options": { + "baseURL": "http://localhost:3001/v1", + "apiKey": "mnfst_YOUR_KEY" + }, + "models": { + "auto": { + "name": "Manifest Auto" + } + } + } + }, + "model": "manifest/auto" + } + ``` + + Replace `http://localhost:3001/v1` with your Manifest instance URL and `mnfst_YOUR_KEY` with your API key. + +3. Run the `/connect` command and search for Manifest. + + ```txt + /connect + ``` + +4. Enter your API key. + + ```txt + ┌ API key + │ + │ + └ enter + ``` + +5. The `auto` model routes requests through Manifest's scoring engine. Run `/models` to confirm it's available. + + ```txt + /models + ``` + +--- + ### SAP AI Core SAP AI Core provides access to 40+ models from OpenAI, Anthropic, Google, Amazon, Meta, Mistral, and AI21 through a unified platform.