Skip to content
Open
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/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { OpenAIPlugin } from "./openai"
import { OpenAICompatiblePlugin } from "./openai-compatible"
import { OpencodePlugin } from "./opencode"
import { OpenRouterPlugin } from "./openrouter"
import { OrcaRouterPlugin } from "./orcarouter"
import { PerplexityPlugin } from "./perplexity"
import { SapAICorePlugin } from "./sap-ai-core"
import { TogetherAIPlugin } from "./togetherai"
Expand Down Expand Up @@ -56,6 +57,7 @@ export const ProviderPlugins = [
OpenAICompatiblePlugin,
OpenAIPlugin,
OpenRouterPlugin,
OrcaRouterPlugin,
PerplexityPlugin,
SapAICorePlugin,
TogetherAIPlugin,
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/plugin/provider/orcarouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Effect } from "effect"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"

export const OrcaRouterPlugin = PluginV2.define({
id: PluginV2.ID.make("orcarouter"),
effect: Effect.gen(function* () {
return {
"provider.update": Effect.fn(function* (evt) {
if (evt.provider.id !== ProviderV2.ID.make("orcarouter")) return
evt.provider.options.headers["HTTP-Referer"] ??= "https://opencode.ai/"
evt.provider.options.headers["X-Title"] ??= "opencode"
}),
}
}),
})
102 changes: 102 additions & 0 deletions packages/core/test/plugin/provider-orcarouter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { OrcaRouterPlugin } from "@opencode-ai/core/plugin/provider/orcarouter"
import { expectPluginRegistered, it, provider } from "./provider-helper"

describe("OrcaRouterPlugin", () => {
it.effect("is registered so legacy referer headers can be applied", () =>
Effect.sync(() =>
expectPluginRegistered(
ProviderPlugins.map((item) => item.id),
"orcarouter",
),
),
)

it.effect("applies legacy referer headers only to orcarouter", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(OrcaRouterPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("orcarouter", {
options: { headers: { Existing: "value" }, body: {}, aisdk: { provider: {}, request: {} } },
}),
cancel: false,
},
)
const ignored = yield* plugin.trigger("provider.update", {}, { provider: provider("openrouter"), cancel: false })
expect(result.provider.options.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect(ignored.provider.options.headers).toEqual({})
}),
)

it.effect("uses the exact OrcaRouter header casing and set", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(OrcaRouterPlugin)
const result = yield* plugin.trigger("provider.update", {}, { provider: provider("orcarouter"), cancel: false })

expect(result.provider.options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect(result.provider.options.headers).not.toHaveProperty("http-referer")
expect(result.provider.options.headers).not.toHaveProperty("x-title")
expect(result.provider.options.headers).not.toHaveProperty("X-Source")
}),
)

it.effect("lets configured OrcaRouter headers override defaults", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(OrcaRouterPlugin)
const result = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("orcarouter", {
options: {
headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" },
body: {},
aisdk: { provider: {}, request: {} },
},
}),
cancel: false,
},
)

expect(result.provider.options.headers).toEqual({
"HTTP-Referer": "https://example.com/",
"X-Title": "custom-title",
})
}),
)

it.effect("guards headers to the exact orcarouter provider id", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(OrcaRouterPlugin)
const ignored = yield* plugin.trigger(
"provider.update",
{},
{
provider: provider("custom-orcarouter", {
endpoint: { type: "aisdk", package: "orcarouter" },
}),
cancel: false,
},
)

expect(ignored.provider.options.headers).toEqual({})
}),
)
})
10 changes: 10 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,16 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
},
},
}),
orcarouter: () =>
Effect.succeed({
autoload: false,
options: {
headers: {
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
},
},
}),
nvidia: (provider) =>
Effect.succeed({
autoload: provider.source === "config",
Expand Down
70 changes: 70 additions & 0 deletions packages/web/src/content/docs/providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1724,6 +1724,76 @@ OpenCode Zen is a list of tested and verified models provided by the OpenCode te

---

### OrcaRouter

[OrcaRouter](https://www.orcarouter.ai) is an OpenAI-compatible router that aggregates 150+ models from OpenAI, Anthropic, Google, xAI, DeepSeek and others behind a single API key, and exposes a virtual `orcarouter/auto` router that picks an upstream per request based on a configurable strategy (`cheapest` / `balanced` / `quality` / `adaptive` / `gated_adaptive`). The adaptive strategy is a LinUCB contextual bandit that learns from your own traffic — same endpoint, but a 50-token summary may be routed to a cheap model while a 30K-token refactor goes to a premium one, without any client-side dispatch logic. Routing strategies, model pools, and reward weights are admin-tunable from [the routing console](https://www.orcarouter.ai/console/routing).

1. Head over to the [OrcaRouter dashboard](https://www.orcarouter.ai/console), click **Create API Key**, and copy the key.

2. Run the `/connect` command and search for OrcaRouter.

```txt
/connect
```

3. Enter the API key for the provider.

```txt
┌ API key
└ enter
```

4. Many OrcaRouter models are preloaded by default, run the `/models` command to select the one you want. The smart-routing entry is `orcarouter/auto`.

```txt
/models
```

You can also add additional models through your opencode config.

```json title="opencode.json" {6}
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"orcarouter": {
"models": {
"somecoolnewmodel": {}
}
}
}
}
```

5. You can also pass OrcaRouter's `extra_body` routing preferences (fallback model list, route mode) per-model. See the [routing docs](https://docs.orcarouter.ai) for the full schema.

```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"orcarouter": {
"models": {
"openai/gpt-5": {
"options": {
"extra_body": {
"models": ["openai/gpt-4o-mini", "openai/gpt-4o"],
"route": "fallback"
}
}
}
}
}
}
}
```

:::note
A few OrcaRouter models (e.g. `anthropic/claude-opus-4.7` and other reasoning-only models) reject `temperature` upstream. If you see a 400 from those models, drop `temperature` from your config.
:::

---

### LLM Gateway

1. Head over to the [LLM Gateway dashboard](https://llmgateway.io/dashboard), click **Create API Key**, and copy the key.
Expand Down
Loading