Skip to content

Commit ab9a485

Browse files
roomote[bot]roomotemrubens
authored
feat: add dynamic model loading for Roo Code Cloud provider (#8728)
Co-authored-by: Roo Code <[email protected]> Co-authored-by: Matt Rubens <[email protected]>
1 parent f4121e2 commit ab9a485

File tree

20 files changed

+429
-117
lines changed

20 files changed

+429
-117
lines changed

packages/types/src/provider-settings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
moonshotModels,
2020
openAiNativeModels,
2121
qwenCodeModels,
22-
rooModels,
2322
sambaNovaModels,
2423
vertexModels,
2524
vscodeLlmModels,
@@ -49,6 +48,7 @@ export const dynamicProviders = [
4948
"requesty",
5049
"unbound",
5150
"glama",
51+
"roo",
5252
] as const
5353

5454
export type DynamicProvider = (typeof dynamicProviders)[number]
@@ -677,7 +677,7 @@ export const MODELS_BY_PROVIDER: Record<
677677
models: Object.keys(openAiNativeModels),
678678
},
679679
"qwen-code": { id: "qwen-code", label: "Qwen Code", models: Object.keys(qwenCodeModels) },
680-
roo: { id: "roo", label: "Roo", models: Object.keys(rooModels) },
680+
roo: { id: "roo", label: "Roo Code Cloud", models: [] },
681681
sambanova: {
682682
id: "sambanova",
683683
label: "SambaNova",
Lines changed: 47 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,49 @@
1+
import { z } from "zod"
2+
13
import type { ModelInfo } from "../model.js"
24

3-
export type RooModelId =
4-
| "xai/grok-code-fast-1"
5-
| "roo/code-supernova-1-million"
6-
| "xai/grok-4-fast"
7-
| "deepseek/deepseek-chat-v3.1"
8-
9-
export const rooDefaultModelId: RooModelId = "xai/grok-code-fast-1"
10-
11-
export const rooModels = {
12-
"xai/grok-code-fast-1": {
13-
maxTokens: 16_384,
14-
contextWindow: 262_144,
15-
supportsImages: false,
16-
supportsPromptCache: true,
17-
inputPrice: 0,
18-
outputPrice: 0,
19-
description:
20-
"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.)",
21-
},
22-
"roo/code-supernova-1-million": {
23-
maxTokens: 30_000,
24-
contextWindow: 1_000_000,
25-
supportsImages: true,
26-
supportsPromptCache: true,
27-
inputPrice: 0,
28-
outputPrice: 0,
29-
description:
30-
"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.)",
31-
},
32-
"xai/grok-4-fast": {
33-
maxTokens: 30_000,
34-
contextWindow: 2_000_000,
35-
supportsImages: false,
36-
supportsPromptCache: false,
37-
inputPrice: 0,
38-
outputPrice: 0,
39-
description:
40-
"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.)",
41-
deprecated: true,
42-
},
43-
"deepseek/deepseek-chat-v3.1": {
44-
maxTokens: 16_384,
45-
contextWindow: 163_840,
46-
supportsImages: false,
47-
supportsPromptCache: false,
48-
inputPrice: 0,
49-
outputPrice: 0,
50-
description:
51-
"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.",
52-
},
53-
} as const satisfies Record<string, ModelInfo>
5+
/**
6+
* Roo Code Cloud is a dynamic provider - models are loaded from the /v1/models API endpoint.
7+
* Default model ID used as fallback when no model is specified.
8+
*/
9+
export const rooDefaultModelId = "xai/grok-code-fast-1"
10+
11+
/**
12+
* Empty models object maintained for type compatibility.
13+
* All model data comes dynamically from the API.
14+
*/
15+
export const rooModels = {} as const satisfies Record<string, ModelInfo>
16+
17+
/**
18+
* Roo Code Cloud API response schemas
19+
*/
20+
21+
export const RooPricingSchema = z.object({
22+
input: z.string(),
23+
output: z.string(),
24+
input_cache_read: z.string().optional(),
25+
input_cache_write: z.string().optional(),
26+
})
27+
28+
export const RooModelSchema = z.object({
29+
id: z.string(),
30+
object: z.literal("model"),
31+
created: z.number(),
32+
owned_by: z.string(),
33+
name: z.string(),
34+
description: z.string(),
35+
context_window: z.number(),
36+
max_tokens: z.number(),
37+
type: z.literal("language"),
38+
tags: z.array(z.string()).optional(),
39+
pricing: RooPricingSchema,
40+
deprecated: z.boolean().optional(),
41+
})
42+
43+
export const RooModelsResponseSchema = z.object({
44+
object: z.literal("list"),
45+
data: z.array(RooModelSchema),
46+
})
47+
48+
export type RooModel = z.infer<typeof RooModelSchema>
49+
export type RooModelsResponse = z.infer<typeof RooModelsResponseSchema>

src/api/providers/__tests__/roo.spec.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// npx vitest run api/providers/__tests__/roo.spec.ts
22

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

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

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

308309
it("should return default model when no model specified", () => {
309310
const handlerWithoutModel = new RooHandler({})
310311
const modelInfo = handlerWithoutModel.getModel()
311312
expect(modelInfo.id).toBe(rooDefaultModelId)
312313
expect(modelInfo.info).toBeDefined()
313-
expect(modelInfo.info).toBe(rooModels[rooDefaultModelId])
314+
// Models are loaded dynamically
315+
expect(modelInfo.info.maxTokens).toBeDefined()
316+
expect(modelInfo.info.contextWindow).toBeDefined()
314317
})
315318

316319
it("should handle unknown model ID with fallback info", () => {
@@ -320,24 +323,27 @@ describe("RooHandler", () => {
320323
const modelInfo = handlerWithUnknownModel.getModel()
321324
expect(modelInfo.id).toBe("unknown-model-id")
322325
expect(modelInfo.info).toBeDefined()
323-
// Should return fallback info for unknown models
324-
expect(modelInfo.info.maxTokens).toBe(16_384)
325-
expect(modelInfo.info.contextWindow).toBe(262_144)
326-
expect(modelInfo.info.supportsImages).toBe(false)
327-
expect(modelInfo.info.supportsPromptCache).toBe(true)
328-
expect(modelInfo.info.inputPrice).toBe(0)
329-
expect(modelInfo.info.outputPrice).toBe(0)
326+
// Should return fallback info for unknown models (dynamic models will be merged in real usage)
327+
expect(modelInfo.info.maxTokens).toBeDefined()
328+
expect(modelInfo.info.contextWindow).toBeDefined()
329+
expect(modelInfo.info.supportsImages).toBeDefined()
330+
expect(modelInfo.info.supportsPromptCache).toBeDefined()
331+
expect(modelInfo.info.inputPrice).toBeDefined()
332+
expect(modelInfo.info.outputPrice).toBeDefined()
330333
})
331334

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

336-
for (const modelId of modelIds) {
339+
for (const modelId of testModelIds) {
337340
const handlerWithModel = new RooHandler({ apiModelId: modelId })
338341
const modelInfo = handlerWithModel.getModel()
339342
expect(modelInfo.id).toBe(modelId)
340-
expect(modelInfo.info).toBe(rooModels[modelId])
343+
expect(modelInfo.info).toBeDefined()
344+
// Verify the structure has required fields
345+
expect(modelInfo.info.maxTokens).toBeDefined()
346+
expect(modelInfo.info.contextWindow).toBeDefined()
341347
}
342348
})
343349
})

src/api/providers/fetchers/modelCache.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { getLMStudioModels } from "./lmstudio"
2424
import { getIOIntelligenceModels } from "./io-intelligence"
2525
import { getDeepInfraModels } from "./deepinfra"
2626
import { getHuggingFaceModels } from "./huggingface"
27+
import { getRooModels } from "./roo"
2728

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

@@ -99,6 +100,13 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
99100
case "huggingface":
100101
models = await getHuggingFaceModels()
101102
break
103+
case "roo": {
104+
// Roo Code Cloud provider requires baseUrl and optional apiKey
105+
const rooBaseUrl =
106+
options.baseUrl ?? process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy"
107+
models = await getRooModels(rooBaseUrl, options.apiKey)
108+
break
109+
}
102110
default: {
103111
// Ensures router is exhaustively checked if RouterName is a strict union.
104112
const exhaustiveCheck: never = provider

src/api/providers/fetchers/roo.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { RooModelsResponseSchema } from "@roo-code/types"
2+
3+
import type { ModelRecord } from "../../../shared/api"
4+
5+
import { DEFAULT_HEADERS } from "../constants"
6+
7+
/**
8+
* Fetches available models from the Roo Code Cloud provider
9+
*
10+
* @param baseUrl The base URL of the Roo Code Cloud provider
11+
* @param apiKey The API key (session token) for the Roo Code Cloud provider
12+
* @returns A promise that resolves to a record of model IDs to model info
13+
* @throws Will throw an error if the request fails or the response is not as expected.
14+
*/
15+
export async function getRooModels(baseUrl: string, apiKey?: string): Promise<ModelRecord> {
16+
try {
17+
const headers: Record<string, string> = {
18+
"Content-Type": "application/json",
19+
...DEFAULT_HEADERS,
20+
}
21+
22+
if (apiKey) {
23+
headers["Authorization"] = `Bearer ${apiKey}`
24+
}
25+
26+
// Construct the models endpoint URL
27+
// Strip trailing /v1 or /v1/ to avoid /v1/v1/models
28+
const normalizedBase = baseUrl.replace(/\/?v1\/?$/, "")
29+
const url = `${normalizedBase}/v1/models`
30+
31+
// Use fetch with AbortController for better timeout handling
32+
const controller = new AbortController()
33+
const timeoutId = setTimeout(() => controller.abort(), 10000)
34+
35+
try {
36+
const response = await fetch(url, {
37+
headers,
38+
signal: controller.signal,
39+
})
40+
41+
if (!response.ok) {
42+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
43+
}
44+
45+
const data = await response.json()
46+
const models: ModelRecord = {}
47+
48+
// Validate response against schema
49+
const parsed = RooModelsResponseSchema.safeParse(data)
50+
51+
if (!parsed.success) {
52+
console.error("Error fetching Roo Code Cloud models: Unexpected response format", data)
53+
console.error("Validation errors:", parsed.error.format())
54+
throw new Error("Failed to fetch Roo Code Cloud models: Unexpected response format.")
55+
}
56+
57+
// Process the validated model data
58+
for (const model of parsed.data.data) {
59+
const modelId = model.id
60+
61+
if (!modelId) continue
62+
63+
// Extract model data from the validated API response
64+
// All required fields are guaranteed by the schema
65+
const contextWindow = model.context_window
66+
const maxTokens = model.max_tokens
67+
const tags = model.tags || []
68+
const pricing = model.pricing
69+
70+
// Determine if the model supports images based on tags
71+
const supportsImages = tags.includes("vision")
72+
73+
// Parse pricing (API returns strings, convert to numbers)
74+
const inputPrice = parseFloat(pricing.input)
75+
const outputPrice = parseFloat(pricing.output)
76+
const cacheReadPrice = pricing.input_cache_read ? parseFloat(pricing.input_cache_read) : undefined
77+
const cacheWritePrice = pricing.input_cache_write ? parseFloat(pricing.input_cache_write) : undefined
78+
79+
models[modelId] = {
80+
maxTokens,
81+
contextWindow,
82+
supportsImages,
83+
supportsPromptCache: Boolean(cacheReadPrice !== undefined),
84+
inputPrice,
85+
outputPrice,
86+
cacheWritesPrice: cacheWritePrice,
87+
cacheReadsPrice: cacheReadPrice,
88+
description: model.description || model.name,
89+
deprecated: model.deprecated || false,
90+
}
91+
}
92+
93+
return models
94+
} finally {
95+
clearTimeout(timeoutId)
96+
}
97+
} catch (error: any) {
98+
console.error("Error fetching Roo Code Cloud models:", error.message ? error.message : error)
99+
100+
// Handle abort/timeout
101+
if (error.name === "AbortError") {
102+
throw new Error("Failed to fetch Roo Code Cloud models: Request timed out after 10 seconds.")
103+
}
104+
105+
// Handle fetch errors
106+
if (error.message?.includes("HTTP")) {
107+
throw new Error(`Failed to fetch Roo Code Cloud models: ${error.message}. Check base URL and API key.`)
108+
}
109+
110+
// Handle network errors
111+
if (error instanceof TypeError) {
112+
throw new Error(
113+
"Failed to fetch Roo Code Cloud models: No response from server. Check Roo Code Cloud server status and base URL.",
114+
)
115+
}
116+
117+
throw new Error(`Failed to fetch Roo Code Cloud models: ${error.message || "An unknown error occurred."}`)
118+
}
119+
}

0 commit comments

Comments
 (0)