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
5 changes: 5 additions & 0 deletions .changeset/orcarouter-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---

Add OrcaRouter as an OpenAI-compatible provider. Set `ORCAROUTER_API_KEY` to route requests through OrcaRouter; requests then carry Kilo Code attribution headers.
11 changes: 11 additions & 0 deletions packages/opencode/src/kilocode/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,17 @@ export function kiloCustomLoaders(dep: CustomDep): Record<string, CustomLoader>
autoload: false,
options: { headers: DEFAULT_HEADERS },
}),

// OrcaRouter is an OpenAI-compatible meta-router (catalog + api come from
// models.dev). Like the other meta-routers (openrouter, vercel, zenmux), it
// only needs Kilo attribution headers so OrcaRouter can attribute traffic to
// Kilo Code. autoload:false defers activation until the user provides
// ORCAROUTER_API_KEY (env detection is handled by the framework).
orcarouter: () =>
Effect.succeed({
autoload: false,
options: { headers: DEFAULT_HEADERS },
}),
}
}

Expand Down
73 changes: 73 additions & 0 deletions packages/opencode/test/kilocode/orcarouter-headers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect } from "bun:test"
import path from "path"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { Env } from "../../src/env"
import { Provider } from "../../src/provider/provider"
import { ProviderID } from "../../src/provider/schema"
import { DEFAULT_HEADERS } from "../../src/kilocode/const"

const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, CrossSpawnSpawner.defaultLayer))

function withOrcaRouterKey<A, E, R>(self: Effect.Effect<A, E, R>) {
return Effect.gen(function* () {
const env = yield* Env.Service
yield* env.set("ORCAROUTER_API_KEY", "test-api-key")
yield* Effect.addFinalizer(() => env.remove("ORCAROUTER_API_KEY"))
return yield* self
})
}

it.live("orcarouter provider includes Kilo Code attribution headers", () =>
provideTmpdirInstance(() =>
withOrcaRouterKey(
Provider.Service.use((provider) =>
Effect.gen(function* () {
const providers = yield* provider.list()
const headers = providers[ProviderID.make("orcarouter")].options.headers

expect(headers["HTTP-Referer"]).toBe(DEFAULT_HEADERS["HTTP-Referer"])
expect(headers["X-Title"]).toBe(DEFAULT_HEADERS["X-Title"])
}),
),
),
),
)

it.live("orcarouter attribution headers can be overridden from config", () =>
provideTmpdirInstance((dir) =>
Effect.gen(function* () {
yield* Effect.promise(() =>
Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://app.kilo.ai/config.json",
provider: {
orcarouter: {
options: {
headers: {
"X-Title": "Custom Title",
},
},
},
},
}),
),
)

return yield* withOrcaRouterKey(
Provider.Service.use((provider) =>
Effect.gen(function* () {
const providers = yield* provider.list()
const headers = providers[ProviderID.make("orcarouter")].options.headers

expect(headers["HTTP-Referer"]).toBe(DEFAULT_HEADERS["HTTP-Referer"])
expect(headers["X-Title"]).toBe("Custom Title")
}),
),
)
}),
),
)
65 changes: 65 additions & 0 deletions packages/opencode/test/tool/fixtures/models-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -28920,6 +28920,71 @@
}
}
},
"orcarouter": {
"id": "orcarouter",
"env": ["ORCAROUTER_API_KEY"],
"npm": "@ai-sdk/openai-compatible",
"api": "https://api.orcarouter.ai/v1",
"name": "OrcaRouter",
"doc": "https://docs.orcarouter.ai",
"models": {
"orcarouter/auto": {
"id": "orcarouter/auto",
"name": "OrcaRouter Auto",
"family": "auto",
"attachment": true,
"reasoning": false,
"tool_call": true,
"temperature": true,
"release_date": "2025-01-01",
"last_updated": "2026-05-14",
"modalities": { "input": ["text", "image"], "output": ["text"] },
"open_weights": false,
"cost": { "input": 0, "output": 0 },
"limit": { "context": 128000, "output": 16384 },
"provider": { "npm": "@ai-sdk/openai-compatible", "api": "https://api.orcarouter.ai/v1" }
},
"openai/gpt-5.5": {
"id": "openai/gpt-5.5",
"name": "GPT-5.5",
"family": "gpt",
"attachment": true,
"reasoning": true,
"tool_call": true,
"temperature": false,
"knowledge": "2025-12-01",
"release_date": "2026-04-23",
"last_updated": "2026-04-23",
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"open_weights": false,
"cost": {
"input": 5,
"output": 30,
"cache_read": 0.5,
"context_over_200k": { "input": 10, "output": 45, "cache_read": 1 }
},
"limit": { "context": 1050000, "input": 922000, "output": 128000 },
"provider": { "npm": "@ai-sdk/openai-compatible", "api": "https://api.orcarouter.ai/v1" }
},
"anthropic/claude-opus-4.7": {
"id": "anthropic/claude-opus-4.7",
"name": "Claude Opus 4.7",
"family": "claude-opus",
"attachment": true,
"reasoning": true,
"tool_call": true,
"temperature": false,
"knowledge": "2026-01-31",
"release_date": "2026-04-16",
"last_updated": "2026-04-16",
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
"open_weights": false,
"cost": { "input": 5, "output": 25, "cache_read": 0.5, "cache_write": 6.25 },
"limit": { "context": 1000000, "output": 128000 },
"provider": { "npm": "@ai-sdk/openai-compatible", "api": "https://api.orcarouter.ai/v1" }
}
}
},
"openrouter": {
"id": "openrouter",
"env": ["OPENROUTER_API_KEY"],
Expand Down