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.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { SnowflakeCortexPlugin } from "./provider/snowflake-cortex"
import { OpenAICompatiblePlugin } from "./provider/openai-compatible"
import { OpencodePlugin } from "./provider/opencode"
import { OpenRouterPlugin } from "./provider/openrouter"
import { OrcaRouterPlugin } from "./provider/orcarouter"
import { PerplexityPlugin } from "./provider/perplexity"
import { SapAICorePlugin } from "./provider/sap-ai-core"
import { TogetherAIPlugin } from "./provider/togetherai"
Expand Down Expand Up @@ -60,6 +61,7 @@ export const ProviderPlugins: PluginInternal.Plugin<PluginInternal.Requirements
OpenAICompatiblePlugin,
OpenAIPlugin,
OpenRouterPlugin,
OrcaRouterPlugin,
PerplexityPlugin,
SapAICorePlugin,
TogetherAIPlugin,
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/plugin/provider/orcarouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Effect } from "effect"
import { define } from "../internal"

export const OrcaRouterPlugin = define({
id: "orcarouter",
effect: Effect.fn(function* (ctx) {
yield* ctx.catalog.transform(
Effect.fn(function* (evt) {
for (const item of evt.provider.list()) {
if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://api.orcarouter.ai/v1") continue
evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] ??= "https://bcode.sh/"
provider.request.headers["X-Title"] ??= "bcode"
})
}
}),
)
}),
})
100 changes: 100 additions & 0 deletions packages/core/test/plugin/provider-orcarouter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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 { OrcaRouterPlugin } from "@opencode-ai/core/plugin/provider/orcarouter"
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* OrcaRouterPlugin.effect(host)
})

function required<T>(value: T | undefined): T {
if (value === undefined) throw new Error("Expected value")
return value
}

const withOrcaRouterProvider = Effect.fn(function* (mutate?: (headers: Record<string, string>) => void) {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("orcarouter"), (provider) => {
provider.api = {
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://api.orcarouter.ai/v1",
}
mutate?.(provider.request.headers)
})
})
return catalog
})

describe("OrcaRouterPlugin", () => {
it.effect("is registered so attribution headers can be applied", () =>
Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("orcarouter"))),
)

it.effect("applies the bcode attribution headers", () =>
Effect.gen(function* () {
const catalog = yield* withOrcaRouterProvider()
yield* addPlugin()
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("orcarouter")))
expect(result.request.headers).toEqual({ "HTTP-Referer": "https://bcode.sh/", "X-Title": "bcode" })
}),
)

it.effect("merges attribution headers with existing headers", () =>
Effect.gen(function* () {
const catalog = yield* withOrcaRouterProvider((headers) => {
headers.Existing = "value"
})
yield* addPlugin()

expect(required(yield* catalog.provider.get(ProviderV2.ID.make("orcarouter"))).request.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://bcode.sh/",
"X-Title": "bcode",
})
}),
)

it.effect("lets configured headers override the defaults", () =>
Effect.gen(function* () {
const catalog = yield* withOrcaRouterProvider((headers) => {
headers["HTTP-Referer"] = "https://example.com/"
headers["X-Title"] = "custom-title"
})
yield* addPlugin()

expect(required(yield* catalog.provider.get(ProviderV2.ID.make("orcarouter"))).request.headers).toEqual({
"HTTP-Referer": "https://example.com/",
"X-Title": "custom-title",
})
}),
)

it.effect("guards attribution headers to the orcarouter base URL", () =>
Effect.gen(function* () {
const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
})
})
yield* addPlugin()

expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({
"HTTP-Referer": "https://example.com/",
"X-Title": "custom-title",
})
}),
)
})
1 change: 1 addition & 0 deletions packages/llm/src/providers/openai-compatible-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const profiles = {
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" },
orcarouter: { provider: "orcarouter", baseURL: "https://api.orcarouter.ai/v1" },
togetherai: { provider: "togetherai", baseURL: "https://api.together.xyz/v1" },
xai: { provider: "xai", baseURL: "https://api.x.ai/v1" },
} as const satisfies Record<string, OpenAICompatibleProfile>
Expand Down
1 change: 1 addition & 0 deletions packages/llm/src/providers/openai-compatible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,5 @@ export const deepinfra = define(profiles.deepinfra)
export const deepseek = define(profiles.deepseek)
export const fireworks = define(profiles.fireworks)
export const groq = define(profiles.groq)
export const orcarouter = define(profiles.orcarouter)
export const togetherai = define(profiles.togetherai)