Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/src/plugin/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -51,6 +52,7 @@ export const ProviderPlugins = [
GroqPlugin,
KiloPlugin,
LLMGatewayPlugin,
ManifestPlugin,
MistralPlugin,
NvidiaPlugin,
OpencodePlugin,
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/plugin/provider/manifest.ts
Original file line number Diff line number Diff line change
@@ -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"
})
}
}),
}
}),
})
1 change: 1 addition & 0 deletions packages/core/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
})),
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -76,6 +77,7 @@ function internalPlugins(flags: RuntimeFlags.Info): PluginInstance[] {
CloudflareAIGatewayAuthPlugin,
AzureAuthPlugin,
DigitalOceanAuthPlugin,
ManifestAuthPlugin,
SnowflakeCortexAuthPlugin,
XaiAuthPlugin,
]
Expand Down
15 changes: 15 additions & 0 deletions packages/opencode/src/plugin/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"

export async function ManifestAuthPlugin(_input: PluginInput): Promise<Hooks> {
return {
auth: {
provider: "manifest",
methods: [
{
type: "api",
label: "Manifest API Key",
},
],
},
}
}
73 changes: 73 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,65 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
},
},
}),
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<string, string> = { 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<string, Model> = {}
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,
Expand Down Expand Up @@ -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)) {
Expand Down
121 changes: 121 additions & 0 deletions packages/opencode/test/provider/manifest.test.ts
Original file line number Diff line number Diff line change
@@ -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 = <A, E, R>(values: Record<string, string>, effect: Effect.Effect<A, E, R>) =>
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<void>((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: {} },
)
})
55 changes: 55 additions & 0 deletions packages/web/src/content/docs/providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading