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/types/src/__tests__/provider-identifiers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {

const expectedProviderIdentifiers = [
"openrouter",
"orcarouter",
"vercel-ai-gateway",
"zoo-gateway",
"litellm",
Expand Down Expand Up @@ -97,6 +98,7 @@ describe("provider identifiers", () => {
it("derives provider category collections from canonical identifiers", () => {
expect(dynamicProviders).toEqual([
providerIdentifiers.openrouter,
providerIdentifiers.orcaRouter,
providerIdentifiers.vercelAiGateway,
providerIdentifiers.zooGateway,
providerIdentifiers.litellm,
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/__tests__/provider-model-id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getModelId, modelIdKeys, providerIdentifiers, type ProviderSettings } f
const expectedModelIdKeys = [
"apiModelId",
"openRouterModelId",
"orcaRouterModelId",
"openAiModelId",
"ollamaModelId",
"lmStudioModelId",
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ export const SECRET_STATE_KEYS = [
"opencodeGoApiKey",
"kenariApiKey",
"nanoGptApiKey",
"orcaRouterApiKey",
"basetenApiKey",
] as const

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/provider-identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
export const providerIdentifiers = {
openrouter: "openrouter",
orcaRouter: "orcarouter",
vercelAiGateway: "vercel-ai-gateway",
zooGateway: "zoo-gateway",
litellm: "litellm",
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3

export const dynamicProviders = [
providerIdentifiers.openrouter,
providerIdentifiers.orcaRouter,
providerIdentifiers.vercelAiGateway,
providerIdentifiers.zooGateway,
providerIdentifiers.litellm,
Expand Down Expand Up @@ -275,6 +276,7 @@ export type ModelIdKey = Extract<keyof ProviderSettingsShape, `${string}ModelId`
export const modelIdKeys = [
"apiModelId",
"openRouterModelId",
"orcaRouterModelId",
"openAiModelId",
"ollamaModelId",
"lmStudioModelId",
Expand Down Expand Up @@ -505,6 +507,7 @@ export const MODELS_BY_PROVIDER: Record<
[providerIdentifiers.poe]: { id: providerIdentifiers.poe, label: "Poe", models: [] },
[providerIdentifiers.litellm]: { id: providerIdentifiers.litellm, label: "LiteLLM", models: [] },
[providerIdentifiers.openrouter]: { id: providerIdentifiers.openrouter, label: "OpenRouter", models: [] },
[providerIdentifiers.orcaRouter]: { id: providerIdentifiers.orcaRouter, label: "OrcaRouter", models: [] },
[providerIdentifiers.requesty]: { id: providerIdentifiers.requesty, label: "Requesty", models: [] },
[providerIdentifiers.unbound]: { id: providerIdentifiers.unbound, label: "Unbound", models: [] },
[providerIdentifiers.vercelAiGateway]: {
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/provider-settings/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { anthropicProviderDefinition } from "./anthropic.js"
import { openRouterProviderDefinition } from "./openrouter.js"
import { orcaRouterProviderDefinition } from "./orca-router.js"
import { bedrockProviderDefinition } from "./bedrock.js"
import { vertexProviderDefinition } from "./vertex.js"
import { openAiProviderDefinition } from "./openai.js"
Expand Down Expand Up @@ -50,6 +51,7 @@ export type { ProviderDefinition } from "./common.js"
export const providerDefinitionList = [
anthropicProviderDefinition,
openRouterProviderDefinition,
orcaRouterProviderDefinition,
bedrockProviderDefinition,
vertexProviderDefinition,
openAiProviderDefinition,
Expand Down
17 changes: 17 additions & 0 deletions packages/types/src/provider-settings/orca-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from "zod"

import { providerIdentifiers } from "../provider-identifiers.js"
import { baseProviderSettingsShape, createModelIdAccessor, createProviderDefinition } from "./common.js"

export const ORCA_ROUTER_MODEL_ID_FIELD = "orcaRouterModelId"

export const orcaRouterProviderDefinition = createProviderDefinition({
apiProvider: providerIdentifiers.orcaRouter,
modelIdKey: ORCA_ROUTER_MODEL_ID_FIELD,
getModelId: createModelIdAccessor(ORCA_ROUTER_MODEL_ID_FIELD),
schema: {
...baseProviderSettingsShape,
orcaRouterApiKey: z.string().optional(),
[ORCA_ROUTER_MODEL_ID_FIELD]: z.string().optional(),
},
})
4 changes: 4 additions & 0 deletions packages/types/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from "./openai.js"
export * from "./openai-codex.js"
export * from "./openai-codex-rate-limits.js"
export * from "./openrouter.js"
export * from "./orca-router.js"
export * from "./poe.js"
export * from "./qwen-code.js"
export * from "./requesty.js"
Expand Down Expand Up @@ -44,6 +45,7 @@ import { mistralDefaultModelId } from "./mistral.js"
import { moonshotDefaultModelId } from "./moonshot.js"
import { openAiCodexDefaultModelId } from "./openai-codex.js"
import { openRouterDefaultModelId } from "./openrouter.js"
import { orcaRouterDefaultModelId } from "./orca-router.js"
import { poeDefaultModelId } from "./poe.js"
import { qwenCodeDefaultModelId } from "./qwen-code.js"
import { requestyDefaultModelId } from "./requesty.js"
Expand Down Expand Up @@ -80,6 +82,8 @@ export function getProviderDefaultModelId(
switch (provider) {
case providerIdentifiers.openrouter:
return openRouterDefaultModelId
case providerIdentifiers.orcaRouter:
return orcaRouterDefaultModelId
case providerIdentifiers.requesty:
return requestyDefaultModelId
case providerIdentifiers.litellm:
Expand Down
18 changes: 18 additions & 0 deletions packages/types/src/providers/orca-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ModelInfo } from "../model.js"

// OrcaRouter's OpenAI-compatible API endpoint.
export const ORCA_ROUTER_BASE_URL = "https://api.orcarouter.ai/v1"

// OrcaRouter mirrors the OpenRouter model namespace (vendor/model), so the same
// model ID shape and defaults apply.
export const orcaRouterDefaultModelId = "orcarouter/fusion-mini"

export const orcaRouterDefaultModelInfo: ModelInfo = {
maxTokens: 128_000,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "OrcaRouter model. Available models and metadata are resolved dynamically from the catalog.",
}
2 changes: 2 additions & 0 deletions src/api/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
OpenAiNativeHandler,
OpencodeGoHandler,
OpenRouterHandler,
OrcaRouterHandler,
PoeHandler,
QwenCodeHandler,
RequestyHandler,
Expand All @@ -72,6 +73,7 @@ type HandlerConstructor = new (...args: never[]) => object
const expectedHandlers = {
[providerIdentifiers.anthropic]: AnthropicHandler,
[providerIdentifiers.openrouter]: OpenRouterHandler,
[providerIdentifiers.orcaRouter]: OrcaRouterHandler,
[providerIdentifiers.bedrock]: AwsBedrockHandler,
[providerIdentifiers.openai]: OpenAiHandler,
[providerIdentifiers.ollama]: NativeOllamaHandler,
Expand Down
3 changes: 3 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
AnthropicHandler,
AwsBedrockHandler,
OpenRouterHandler,
OrcaRouterHandler,
PoeHandler,
VertexHandler,
AnthropicVertexHandler,
Expand Down Expand Up @@ -169,6 +170,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
return new AnthropicHandler(options)
case providerIdentifiers.openrouter:
return new OpenRouterHandler(options)
case providerIdentifiers.orcaRouter:
return new OrcaRouterHandler(options)
case providerIdentifiers.bedrock:
return new AwsBedrockHandler(options)
case providerIdentifiers.vertex:
Expand Down
8 changes: 8 additions & 0 deletions src/api/providers/fetchers/modelCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { RouterName } from "../../../shared/api"
import { fileExistsAtPath } from "../../../utils/fs"

import { getOpenRouterModels } from "./openrouter"
import { getOrcaRouterModels } from "./orca-router"
import { getVercelAiGatewayModels } from "./vercel-ai-gateway"
import { getOpencodeGoModels } from "./opencode-go"
import { getKenariModels } from "./kenari"
Expand Down Expand Up @@ -230,6 +231,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise<Model
case providerIdentifiers.openrouter:
models = await getOpenRouterModels()
break
case providerIdentifiers.orcaRouter:
models = await getOrcaRouterModels(options.apiKey)
break
case providerIdentifiers.requesty:
// Requesty models endpoint requires an API key for per-user custom policies.
models = await getRequestyModels(options.baseUrl, options.apiKey)
Expand Down Expand Up @@ -459,6 +463,10 @@ export async function initializeModelCacheRefresh(): Promise<void> {
provider: providerIdentifiers.openrouter,
options: { provider: providerIdentifiers.openrouter },
},
{
provider: providerIdentifiers.orcaRouter,
options: { provider: providerIdentifiers.orcaRouter },
},
{
provider: providerIdentifiers.vercelAiGateway,
options: { provider: providerIdentifiers.vercelAiGateway },
Expand Down
83 changes: 83 additions & 0 deletions src/api/providers/fetchers/orca-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import axios from "axios"
import { z } from "zod"

import { ORCA_ROUTER_BASE_URL, orcaRouterDefaultModelInfo, type ModelInfo, type ModelRecord } from "@roo-code/types"

const orcaRouterModelSchema = z.object({
id: z.string().min(1),
name: z.string().optional(),
description: z.string().optional(),
context_length: z.number().positive().nullish(),
max_completion_tokens: z.number().positive().nullish(),
architecture: z
.object({
input_modalities: z.array(z.string()).optional(),
})
.optional(),
pricing: z
.object({
prompt: z.string().nullish(),
completion: z.string().nullish(),
})
.optional(),
})

const orcaRouterModelsResponseSchema = z.object({
data: z.array(z.unknown()),
})

function parseApiPrice(value: string | null | undefined): number | undefined {
if (value === null || value === undefined) {
return undefined
}

const price = Number(value)
return Number.isFinite(price) ? price : undefined
}

export const parseOrcaRouterModel = (model: z.infer<typeof orcaRouterModelSchema>): ModelInfo => ({
contextWindow: model.context_length ?? orcaRouterDefaultModelInfo.contextWindow,
maxTokens: model.max_completion_tokens ?? orcaRouterDefaultModelInfo.maxTokens,
supportsPromptCache: false,
supportsImages: model.architecture?.input_modalities?.includes("image") ?? false,
...(model.name !== undefined ? { displayName: model.name } : {}),
...(model.description !== undefined ? { description: model.description } : {}),
...(model.pricing?.prompt !== undefined ? { inputPrice: parseApiPrice(model.pricing.prompt) } : {}),
...(model.pricing?.completion !== undefined ? { outputPrice: parseApiPrice(model.pricing.completion) } : {}),
})

/**
* Fetches OrcaRouter's public model catalog. The endpoint is unauthenticated,
* so the API key is optional and only forwarded when present.
*/
export async function getOrcaRouterModels(apiKey?: string): Promise<ModelRecord> {
try {
const response = await axios.get(`${ORCA_ROUTER_BASE_URL}/models`, {
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
timeout: 10_000,
})
const responseResult = orcaRouterModelsResponseSchema.safeParse(response.data)
if (!responseResult.success) {
console.warn("OrcaRouter models response did not match the expected top-level schema")
return {}
}

const models: ModelRecord = {}
for (const rawModel of responseResult.data.data) {
const modelResult = orcaRouterModelSchema.safeParse(rawModel)
if (!modelResult.success) {
continue
}

models[modelResult.data.id] = parseOrcaRouterModel(modelResult.data)
}

return models
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(
`Error fetching OrcaRouter models: ${apiKey ? message.replaceAll(apiKey, "[REDACTED]") : message}`,
)
return {}
}
}
1 change: 1 addition & 0 deletions src/api/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export { OpenAiHandler } from "./openai"
export { OpenAICompatibleHandler } from "./openai-compatible"
export type { OpenAICompatibleConfig } from "./openai-compatible"
export { OpenRouterHandler } from "./openrouter"
export { OrcaRouterHandler } from "./orca-router"
export { PoeHandler } from "./poe"
export { QwenCodeHandler } from "./qwen-code"
export { RequestyHandler } from "./requesty"
Expand Down
Loading