Skip to content

Commit 382764c

Browse files
committed
Make it work
1 parent 626d97b commit 382764c

File tree

14 files changed

+192
-127
lines changed

14 files changed

+192
-127
lines changed

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 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,
@@ -678,7 +677,7 @@ export const MODELS_BY_PROVIDER: Record<
678677
models: Object.keys(openAiNativeModels),
679678
},
680679
"qwen-code": { id: "qwen-code", label: "Qwen Code", models: Object.keys(qwenCodeModels) },
681-
roo: { id: "roo", label: "Roo", models: Object.keys(rooModels) },
680+
roo: { id: "roo", label: "Roo Code Cloud", models: [] },
682681
sambanova: {
683682
id: "sambanova",
684683
label: "SambaNova",
Lines changed: 10 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,13 @@
11
import type { ModelInfo } from "../model.js"
22

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"
3+
/**
4+
* Roo Code Cloud is a dynamic provider - models are loaded from the /v1/models API endpoint.
5+
* Default model ID used as fallback when no model is specified.
6+
*/
7+
export const rooDefaultModelId = "xai/grok-code-fast-1"
88

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>
9+
/**
10+
* Empty models object maintained for type compatibility.
11+
* All model data comes dynamically from the API.
12+
*/
13+
export const rooModels = {} as const satisfies Record<string, ModelInfo>

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/roo.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,33 +24,54 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
2424
}
2525

2626
// Normalize the URL to ensure proper /v1/models endpoint construction
27+
// Remove any trailing /v1 to avoid duplication
2728
const urlObj = new URL(baseUrl)
28-
urlObj.pathname = urlObj.pathname.replace(/\/+$/, "").replace(/\/+/g, "/") + "/v1/models"
29+
let pathname = urlObj.pathname.replace(/\/+$/, "").replace(/\/+/g, "/")
30+
// Remove trailing /v1 if present to avoid /v1/v1/models
31+
if (pathname.endsWith("/v1")) {
32+
pathname = pathname.slice(0, -3)
33+
}
34+
urlObj.pathname = pathname + "/v1/models"
2935
const url = urlObj.href
3036

3137
// Added timeout to prevent indefinite hanging
3238
const response = await axios.get(url, { headers, timeout: 10000 })
3339
const models: ModelRecord = {}
34-
3540
// Process the model info from the response
36-
// Expected format: { object: "list", data: [{ id: string, object: "model", created: number, owned_by: string }] }
41+
// Expected format: { object: "list", data: [{ id, name, description, context_window, max_tokens, tags, pricing }] }
3742
if (response.data && response.data.data && Array.isArray(response.data.data)) {
3843
for (const model of response.data.data) {
3944
const modelId = model.id
4045

4146
if (!modelId) continue
4247

43-
// For Roo Code Cloud, we provide basic model info
44-
// The actual detailed model info is stored in the static rooModels definition
45-
// This just confirms which models are available
48+
// Extract model data from the API response
49+
const contextWindow = model.context_window || 262_144
50+
const maxTokens = model.max_tokens || 16_384
51+
const tags = model.tags || []
52+
const pricing = model.pricing || {}
53+
54+
// Determine if the model supports images based on tags
55+
const supportsImages = tags.includes("vision") || tags.includes("image")
56+
57+
// Parse pricing (API returns strings, convert to numbers)
58+
// Handle both direct pricing and cache pricing if available
59+
const inputPrice = pricing.input ? parseFloat(pricing.input) : 0
60+
const outputPrice = pricing.output ? parseFloat(pricing.output) : 0
61+
const cacheReadPrice = pricing.input_cache_read ? parseFloat(pricing.input_cache_read) : undefined
62+
const cacheWritePrice = pricing.input_cache_write ? parseFloat(pricing.input_cache_write) : undefined
63+
4664
models[modelId] = {
47-
maxTokens: 16_384, // Default fallback
48-
contextWindow: 262_144, // Default fallback
49-
supportsImages: false,
50-
supportsPromptCache: true,
51-
inputPrice: 0,
52-
outputPrice: 0,
53-
description: `Model available through Roo Code Cloud`,
65+
maxTokens,
66+
contextWindow,
67+
supportsImages,
68+
supportsPromptCache: Boolean(cacheReadPrice !== undefined || cacheWritePrice !== undefined),
69+
inputPrice,
70+
outputPrice,
71+
cacheWritesPrice: cacheWritePrice,
72+
cacheReadsPrice: cacheReadPrice,
73+
description: model.description || model.name || `Model available through Roo Code Cloud`,
74+
deprecated: model.deprecated || false,
5475
}
5576
}
5677
} else {

src/api/providers/roo.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import OpenAI from "openai"
33

4-
import { AuthState, rooDefaultModelId, rooModels, type RooModelId, type ModelInfo } from "@roo-code/types"
4+
import { AuthState, rooDefaultModelId, type ModelInfo } from "@roo-code/types"
55
import { CloudService } from "@roo-code/cloud"
66

77
import type { ApiHandlerOptions, ModelRecord } from "../../shared/api"
@@ -14,7 +14,7 @@ import { getModels } from "../providers/fetchers/modelCache"
1414

1515
export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
1616
private authStateListener?: (state: { state: AuthState }) => void
17-
private mergedModels: Record<string, ModelInfo> = rooModels as Record<string, ModelInfo>
17+
private mergedModels: Record<string, ModelInfo> = {}
1818
private modelsLoaded = false
1919

2020
constructor(options: ApiHandlerOptions) {
@@ -31,14 +31,14 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
3131
super({
3232
...options,
3333
providerName: "Roo Code Cloud",
34-
baseURL,
34+
baseURL: `${baseURL}/v1`, // OpenAI client needs /v1 suffix
3535
apiKey: sessionToken || "unauthenticated", // Use a placeholder if no token.
3636
defaultProviderModelId: rooDefaultModelId,
37-
providerModels: rooModels as Record<string, ModelInfo>,
37+
providerModels: {},
3838
defaultTemperature: 0.7,
3939
})
4040

41-
// Load dynamic models asynchronously
41+
// Load dynamic models asynchronously - pass base URL without /v1
4242
this.loadDynamicModels(baseURL, sessionToken).catch((error) => {
4343
console.error("[RooHandler] Failed to load dynamic models:", error)
4444
})
@@ -122,8 +122,8 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
122122
})
123123
this.modelsLoaded = true
124124

125-
// Merge dynamic models with static models, preferring static model info
126-
this.mergedModels = { ...dynamicModels, ...rooModels } as Record<string, ModelInfo>
125+
// Use dynamic models directly - no static fallbacks needed
126+
this.mergedModels = dynamicModels as Record<string, ModelInfo>
127127
} catch (error) {
128128
console.error("[RooHandler] Error loading dynamic models:", error)
129129
// Keep using static models as fallback

src/core/config/ProviderSettingsManager.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
1212
getModelId,
1313
type ProviderName,
14-
type RooModelId,
1514
} from "@roo-code/types"
1615
import { TelemetryService } from "@roo-code/telemetry"
1716

@@ -24,7 +23,7 @@ type ModelMigrations = {
2423

2524
const MODEL_MIGRATIONS: ModelMigrations = {
2625
roo: {
27-
"roo/code-supernova": "roo/code-supernova-1-million" as RooModelId,
26+
"roo/code-supernova": "roo/code-supernova-1-million",
2827
},
2928
} as const satisfies ModelMigrations
3029

src/core/webview/__tests__/webviewMessageHandler.spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,18 @@ describe("webviewMessageHandler - requestRouterModels", () => {
218218
})
219219

220220
// Verify getModels was called for each provider
221-
expect(mockGetModels).toHaveBeenCalledWith({ provider: "deepinfra" })
222221
expect(mockGetModels).toHaveBeenCalledWith({ provider: "openrouter" })
223222
expect(mockGetModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
224223
expect(mockGetModels).toHaveBeenCalledWith({ provider: "glama" })
225224
expect(mockGetModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
226225
expect(mockGetModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" })
226+
expect(mockGetModels).toHaveBeenCalledWith({ provider: "deepinfra" })
227+
expect(mockGetModels).toHaveBeenCalledWith(
228+
expect.objectContaining({
229+
provider: "roo",
230+
baseUrl: expect.any(String),
231+
}),
232+
)
227233
expect(mockGetModels).toHaveBeenCalledWith({
228234
provider: "litellm",
229235
apiKey: "litellm-key",
@@ -242,6 +248,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
242248
glama: mockModels,
243249
unbound: mockModels,
244250
litellm: mockModels,
251+
roo: mockModels,
245252
ollama: {},
246253
lmstudio: {},
247254
"vercel-ai-gateway": mockModels,
@@ -332,6 +339,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
332339
requesty: mockModels,
333340
glama: mockModels,
334341
unbound: mockModels,
342+
roo: mockModels,
335343
litellm: {},
336344
ollama: {},
337345
lmstudio: {},
@@ -360,6 +368,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
360368
.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
361369
.mockResolvedValueOnce(mockModels) // vercel-ai-gateway
362370
.mockResolvedValueOnce(mockModels) // deepinfra
371+
.mockResolvedValueOnce(mockModels) // roo
363372
.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
364373

365374
await webviewMessageHandler(mockClineProvider, {
@@ -375,6 +384,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
375384
requesty: {},
376385
glama: mockModels,
377386
unbound: {},
387+
roo: mockModels,
378388
litellm: {},
379389
ollama: {},
380390
lmstudio: {},
@@ -416,6 +426,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
416426
.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
417427
.mockRejectedValueOnce(new Error("Vercel AI Gateway error")) // vercel-ai-gateway
418428
.mockRejectedValueOnce(new Error("DeepInfra API error")) // deepinfra
429+
.mockRejectedValueOnce(new Error("Roo API error")) // roo
419430
.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
420431

421432
await webviewMessageHandler(mockClineProvider, {
@@ -458,6 +469,20 @@ describe("webviewMessageHandler - requestRouterModels", () => {
458469
values: { provider: "deepinfra" },
459470
})
460471

472+
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
473+
type: "singleRouterModelFetchResponse",
474+
success: false,
475+
error: "Vercel AI Gateway error",
476+
values: { provider: "vercel-ai-gateway" },
477+
})
478+
479+
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
480+
type: "singleRouterModelFetchResponse",
481+
success: false,
482+
error: "Roo API error",
483+
values: { provider: "roo" },
484+
})
485+
461486
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
462487
type: "singleRouterModelFetchResponse",
463488
success: false,

src/core/webview/webviewMessageHandler.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,16 @@ export const webviewMessageHandler = async (
805805
baseUrl: apiConfiguration.deepInfraBaseUrl,
806806
},
807807
},
808+
{
809+
key: "roo",
810+
options: {
811+
provider: "roo",
812+
baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy",
813+
apiKey: CloudService.hasInstance()
814+
? CloudService.instance.authService?.getSessionToken()
815+
: undefined,
816+
},
817+
},
808818
]
809819

810820
// Add IO Intelligence if API key is provided.

0 commit comments

Comments
 (0)