Skip to content
4 changes: 2 additions & 2 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
moonshotModels,
openAiNativeModels,
qwenCodeModels,
rooModels,
sambaNovaModels,
vertexModels,
vscodeLlmModels,
Expand Down Expand Up @@ -49,6 +48,7 @@ export const dynamicProviders = [
"requesty",
"unbound",
"glama",
"roo",
] as const

export type DynamicProvider = (typeof dynamicProviders)[number]
Expand Down Expand Up @@ -677,7 +677,7 @@ export const MODELS_BY_PROVIDER: Record<
models: Object.keys(openAiNativeModels),
},
"qwen-code": { id: "qwen-code", label: "Qwen Code", models: Object.keys(qwenCodeModels) },
roo: { id: "roo", label: "Roo", models: Object.keys(rooModels) },
roo: { id: "roo", label: "Roo Code Cloud", models: [] },
sambanova: {
id: "sambanova",
label: "SambaNova",
Expand Down
98 changes: 47 additions & 51 deletions packages/types/src/providers/roo.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,49 @@
import { z } from "zod"

import type { ModelInfo } from "../model.js"

export type RooModelId =
| "xai/grok-code-fast-1"
| "roo/code-supernova-1-million"
| "xai/grok-4-fast"
| "deepseek/deepseek-chat-v3.1"

export const rooDefaultModelId: RooModelId = "xai/grok-code-fast-1"

export const rooModels = {
"xai/grok-code-fast-1": {
maxTokens: 16_384,
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0,
description:
"A reasoning model that is blazing fast and excels at agentic coding, accessible for free through Roo Code Cloud for a limited time. (Note: the free prompts and completions are logged by xAI and used to improve the model.)",
},
"roo/code-supernova-1-million": {
maxTokens: 30_000,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0,
description:
"A versatile agentic coding stealth model with a 1M token context window that supports image inputs, accessible for free through Roo Code Cloud for a limited time. (Note: the free prompts and completions are logged by the model provider and used to improve the model.)",
},
"xai/grok-4-fast": {
maxTokens: 30_000,
contextWindow: 2_000_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description:
"Grok 4 Fast is xAI's latest multimodal model with SOTA cost-efficiency and a 2M token context window. (Note: prompts and completions are logged by xAI and used to improve the model.)",
deprecated: true,
},
"deepseek/deepseek-chat-v3.1": {
maxTokens: 16_384,
contextWindow: 163_840,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description:
"DeepSeek-V3.1 is a large hybrid reasoning model (671B parameters, 37B active). It extends the DeepSeek-V3 base with a two-phase long-context training process, reaching up to 128K tokens, and uses FP8 microscaling for efficient inference.",
},
} as const satisfies Record<string, ModelInfo>
/**
* Roo Code Cloud is a dynamic provider - models are loaded from the /v1/models API endpoint.
* Default model ID used as fallback when no model is specified.
*/
export const rooDefaultModelId = "xai/grok-code-fast-1"

/**
* Empty models object maintained for type compatibility.
* All model data comes dynamically from the API.
*/
export const rooModels = {} as const satisfies Record<string, ModelInfo>

/**
* Roo Code Cloud API response schemas
*/

export const RooPricingSchema = z.object({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need some kind of tiered pricing support (or is that allowed with this pattern/standard)?

input: z.string(),
output: z.string(),
input_cache_read: z.string().optional(),
input_cache_write: z.string().optional(),
})

export const RooModelSchema = z.object({
id: z.string(),
object: z.literal("model"),
created: z.number(),
owned_by: z.string(),
name: z.string(),
description: z.string(),
context_window: z.number(),
max_tokens: z.number(),
type: z.literal("language"),
tags: z.array(z.string()).optional(),
pricing: RooPricingSchema,
deprecated: z.boolean().optional(),
})

export const RooModelsResponseSchema = z.object({
object: z.literal("list"),
data: z.array(RooModelSchema),
})

export type RooModel = z.infer<typeof RooModelSchema>
export type RooModelsResponse = z.infer<typeof RooModelsResponseSchema>
38 changes: 22 additions & 16 deletions src/api/providers/__tests__/roo.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// npx vitest run api/providers/__tests__/roo.spec.ts

import { Anthropic } from "@anthropic-ai/sdk"
import { rooDefaultModelId, rooModels } from "@roo-code/types"
import { rooDefaultModelId } from "@roo-code/types"

import { ApiHandlerOptions } from "../../../shared/api"

Expand Down Expand Up @@ -301,16 +301,19 @@ describe("RooHandler", () => {
const modelInfo = handler.getModel()
expect(modelInfo.id).toBe(mockOptions.apiModelId)
expect(modelInfo.info).toBeDefined()
// xai/grok-code-fast-1 is a valid model in rooModels
expect(modelInfo.info).toBe(rooModels["xai/grok-code-fast-1"])
// Models are loaded dynamically, so we just verify the structure
expect(modelInfo.info.maxTokens).toBeDefined()
expect(modelInfo.info.contextWindow).toBeDefined()
})

it("should return default model when no model specified", () => {
const handlerWithoutModel = new RooHandler({})
const modelInfo = handlerWithoutModel.getModel()
expect(modelInfo.id).toBe(rooDefaultModelId)
expect(modelInfo.info).toBeDefined()
expect(modelInfo.info).toBe(rooModels[rooDefaultModelId])
// Models are loaded dynamically
expect(modelInfo.info.maxTokens).toBeDefined()
expect(modelInfo.info.contextWindow).toBeDefined()
})

it("should handle unknown model ID with fallback info", () => {
Expand All @@ -320,24 +323,27 @@ describe("RooHandler", () => {
const modelInfo = handlerWithUnknownModel.getModel()
expect(modelInfo.id).toBe("unknown-model-id")
expect(modelInfo.info).toBeDefined()
// Should return fallback info for unknown models
expect(modelInfo.info.maxTokens).toBe(16_384)
expect(modelInfo.info.contextWindow).toBe(262_144)
expect(modelInfo.info.supportsImages).toBe(false)
expect(modelInfo.info.supportsPromptCache).toBe(true)
expect(modelInfo.info.inputPrice).toBe(0)
expect(modelInfo.info.outputPrice).toBe(0)
// Should return fallback info for unknown models (dynamic models will be merged in real usage)
expect(modelInfo.info.maxTokens).toBeDefined()
expect(modelInfo.info.contextWindow).toBeDefined()
expect(modelInfo.info.supportsImages).toBeDefined()
expect(modelInfo.info.supportsPromptCache).toBeDefined()
expect(modelInfo.info.inputPrice).toBeDefined()
expect(modelInfo.info.outputPrice).toBeDefined()
})

it("should return correct model info for all Roo models", () => {
// Test each model in rooModels
const modelIds = Object.keys(rooModels) as Array<keyof typeof rooModels>
it("should handle any model ID since models are loaded dynamically", () => {
// Test with various model IDs - they should all work since models are loaded dynamically
const testModelIds = ["xai/grok-code-fast-1", "roo/sonic", "deepseek/deepseek-chat-v3.1"]

for (const modelId of modelIds) {
for (const modelId of testModelIds) {
const handlerWithModel = new RooHandler({ apiModelId: modelId })
const modelInfo = handlerWithModel.getModel()
expect(modelInfo.id).toBe(modelId)
expect(modelInfo.info).toBe(rooModels[modelId])
expect(modelInfo.info).toBeDefined()
// Verify the structure has required fields
expect(modelInfo.info.maxTokens).toBeDefined()
expect(modelInfo.info.contextWindow).toBeDefined()
}
})
})
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 @@ -24,6 +24,7 @@ import { getLMStudioModels } from "./lmstudio"
import { getIOIntelligenceModels } from "./io-intelligence"
import { getDeepInfraModels } from "./deepinfra"
import { getHuggingFaceModels } from "./huggingface"
import { getRooModels } from "./roo"

const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })

Expand Down Expand Up @@ -99,6 +100,13 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
case "huggingface":
models = await getHuggingFaceModels()
break
case "roo": {
// Roo Code Cloud provider requires baseUrl and optional apiKey
const rooBaseUrl =
options.baseUrl ?? process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy"
models = await getRooModels(rooBaseUrl, options.apiKey)
break
}
default: {
// Ensures router is exhaustively checked if RouterName is a strict union.
const exhaustiveCheck: never = provider
Expand Down
119 changes: 119 additions & 0 deletions src/api/providers/fetchers/roo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { RooModelsResponseSchema } from "@roo-code/types"

import type { ModelRecord } from "../../../shared/api"

import { DEFAULT_HEADERS } from "../constants"

/**
* Fetches available models from the Roo Code Cloud provider
*
* @param baseUrl The base URL of the Roo Code Cloud provider
* @param apiKey The API key (session token) for the Roo Code Cloud provider
* @returns A promise that resolves to a record of model IDs to model info
* @throws Will throw an error if the request fails or the response is not as expected.
*/
export async function getRooModels(baseUrl: string, apiKey?: string): Promise<ModelRecord> {
try {
const headers: Record<string, string> = {
"Content-Type": "application/json",
...DEFAULT_HEADERS,
}

if (apiKey) {
headers["Authorization"] = `Bearer ${apiKey}`
}

// Construct the models endpoint URL
// Strip trailing /v1 or /v1/ to avoid /v1/v1/models
const normalizedBase = baseUrl.replace(/\/?v1\/?$/, "")
const url = `${normalizedBase}/v1/models`

// Use fetch with AbortController for better timeout handling
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000)

try {
const response = await fetch(url, {
headers,
signal: controller.signal,
})

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}

const data = await response.json()
const models: ModelRecord = {}

// Validate response against schema
const parsed = RooModelsResponseSchema.safeParse(data)

if (!parsed.success) {
console.error("Error fetching Roo Code Cloud models: Unexpected response format", data)
console.error("Validation errors:", parsed.error.format())
throw new Error("Failed to fetch Roo Code Cloud models: Unexpected response format.")
}

// Process the validated model data
for (const model of parsed.data.data) {
const modelId = model.id

if (!modelId) continue

// Extract model data from the validated API response
// All required fields are guaranteed by the schema
const contextWindow = model.context_window
const maxTokens = model.max_tokens
const tags = model.tags || []
const pricing = model.pricing

// Determine if the model supports images based on tags
const supportsImages = tags.includes("vision")

// Parse pricing (API returns strings, convert to numbers)
const inputPrice = parseFloat(pricing.input)
const outputPrice = parseFloat(pricing.output)
const cacheReadPrice = pricing.input_cache_read ? parseFloat(pricing.input_cache_read) : undefined
const cacheWritePrice = pricing.input_cache_write ? parseFloat(pricing.input_cache_write) : undefined

models[modelId] = {
maxTokens,
contextWindow,
supportsImages,
supportsPromptCache: Boolean(cacheReadPrice !== undefined),
inputPrice,
outputPrice,
cacheWritesPrice: cacheWritePrice,
cacheReadsPrice: cacheReadPrice,
description: model.description || model.name,
deprecated: model.deprecated || false,
}
}

return models
} finally {
clearTimeout(timeoutId)
}
} catch (error: any) {
console.error("Error fetching Roo Code Cloud models:", error.message ? error.message : error)

// Handle abort/timeout
if (error.name === "AbortError") {
throw new Error("Failed to fetch Roo Code Cloud models: Request timed out after 10 seconds.")
}

// Handle fetch errors
if (error.message?.includes("HTTP")) {
throw new Error(`Failed to fetch Roo Code Cloud models: ${error.message}. Check base URL and API key.`)
}

// Handle network errors
if (error instanceof TypeError) {
throw new Error(
"Failed to fetch Roo Code Cloud models: No response from server. Check Roo Code Cloud server status and base URL.",
)
}

throw new Error(`Failed to fetch Roo Code Cloud models: ${error.message || "An unknown error occurred."}`)
}
}
Loading