feat: add dynamic model loading for Roo Code Cloud provider (#8728)

Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
roomote[bot] 2025-10-24 00:58:34 -04:00 committed by GitHub
parent f4121e2568
commit ab9a48578c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 427 additions and 115 deletions

View file

@ -19,7 +19,6 @@ import {
moonshotModels,
openAiNativeModels,
qwenCodeModels,
rooModels,
sambaNovaModels,
vertexModels,
vscodeLlmModels,
@ -49,6 +48,7 @@ export const dynamicProviders = [
"requesty",
"unbound",
"glama",
"roo",
] as const
export type DynamicProvider = (typeof dynamicProviders)[number]
@ -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",

View file

@ -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"
/**
* 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"
export const rooDefaultModelId: RooModelId = "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>
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 API response schemas
*/
export const RooPricingSchema = z.object({
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>

View file

@ -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"
@ -301,8 +301,9 @@ 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", () => {
@ -310,7 +311,9 @@ describe("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", () => {
@ -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()
}
})
})

View file

@ -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 })
@ -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

View file

@ -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."}`)
}
}

View file

@ -1,18 +1,20 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { AuthState, rooDefaultModelId, rooModels, type RooModelId } from "@roo-code/types"
import { AuthState, rooDefaultModelId, type ModelInfo } from "@roo-code/types"
import { CloudService } from "@roo-code/cloud"
import type { ApiHandlerOptions } from "../../shared/api"
import type { ApiHandlerOptions, ModelRecord } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import type { ApiHandlerCreateMessageMetadata } from "../index"
import { DEFAULT_HEADERS } from "./constants"
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
import { getModels, flushModels, getModelsFromCache } from "../providers/fetchers/modelCache"
export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
private authStateListener?: (state: { state: AuthState }) => void
private fetcherBaseURL: string
constructor(options: ApiHandlerOptions) {
let sessionToken: string | undefined = undefined
@ -21,34 +23,62 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
sessionToken = CloudService.instance.authService?.getSessionToken()
}
let baseURL = process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy"
// Ensure baseURL ends with /v1 for OpenAI client, but don't duplicate it
if (!baseURL.endsWith("/v1")) {
baseURL = `${baseURL}/v1`
}
// Always construct the handler, even without a valid token.
// The provider-proxy server will return 401 if authentication fails.
super({
...options,
providerName: "Roo Code Cloud",
baseURL: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy/v1",
baseURL, // Already has /v1 suffix
apiKey: sessionToken || "unauthenticated", // Use a placeholder if no token.
defaultProviderModelId: rooDefaultModelId,
providerModels: rooModels,
providerModels: {},
defaultTemperature: 0.7,
})
// Load dynamic models asynchronously - strip /v1 from baseURL for fetcher
this.fetcherBaseURL = baseURL.endsWith("/v1") ? baseURL.slice(0, -3) : baseURL
this.loadDynamicModels(this.fetcherBaseURL, sessionToken).catch((error) => {
console.error("[RooHandler] Failed to load dynamic models:", error)
})
if (CloudService.hasInstance()) {
const cloudService = CloudService.instance
this.authStateListener = (state: { state: AuthState }) => {
if (state.state === "active-session") {
const newToken = cloudService.authService?.getSessionToken()
this.client = new OpenAI({
baseURL: this.baseURL,
apiKey: cloudService.authService?.getSessionToken() ?? "unauthenticated",
apiKey: newToken ?? "unauthenticated",
defaultHeaders: DEFAULT_HEADERS,
})
// Flush cache and reload models with the new auth token
flushModels("roo")
.then(() => {
return this.loadDynamicModels(this.fetcherBaseURL, newToken)
})
.catch((error) => {
console.error("[RooHandler] Failed to reload models after auth:", error)
})
} else if (state.state === "logged-out") {
this.client = new OpenAI({
baseURL: this.baseURL,
apiKey: "unauthenticated",
defaultHeaders: DEFAULT_HEADERS,
})
// Flush cache when logged out
flushModels("roo").catch((error) => {
console.error("[RooHandler] Failed to flush models on logout:", error)
})
}
}
@ -103,17 +133,33 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
}
}
private async loadDynamicModels(baseURL: string, apiKey?: string): Promise<void> {
try {
// Fetch models and cache them in the shared cache
await getModels({
provider: "roo",
baseUrl: baseURL,
apiKey,
})
} catch (error) {
console.error("[RooHandler] Error loading dynamic models:", error)
}
}
override getModel() {
const modelId = this.options.apiModelId || rooDefaultModelId
const modelInfo = this.providerModels[modelId as RooModelId] ?? this.providerModels[rooDefaultModelId]
// Get models from shared cache
const models = getModelsFromCache("roo") || {}
const modelInfo = models[modelId]
if (modelInfo) {
return { id: modelId as RooModelId, info: modelInfo }
return { id: modelId, info: modelInfo }
}
// Return the requested model ID even if not found, with fallback info.
return {
id: modelId as RooModelId,
id: modelId,
info: {
maxTokens: 16_384,
contextWindow: 262_144,

View file

@ -11,7 +11,6 @@ import {
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
getModelId,
type ProviderName,
type RooModelId,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
@ -24,7 +23,7 @@ type ModelMigrations = {
const MODEL_MIGRATIONS: ModelMigrations = {
roo: {
"roo/code-supernova": "roo/code-supernova-1-million" as RooModelId,
"roo/code-supernova": "roo/code-supernova-1-million",
},
} as const satisfies ModelMigrations

View file

@ -2689,6 +2689,13 @@ describe("ClineProvider - Router Models", () => {
expect(getModels).toHaveBeenCalledWith({ provider: "glama" })
expect(getModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
expect(getModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" })
expect(getModels).toHaveBeenCalledWith({ provider: "deepinfra" })
expect(getModels).toHaveBeenCalledWith(
expect.objectContaining({
provider: "roo",
baseUrl: expect.any(String),
}),
)
expect(getModels).toHaveBeenCalledWith({
provider: "litellm",
apiKey: "litellm-key",
@ -2704,6 +2711,7 @@ describe("ClineProvider - Router Models", () => {
requesty: mockModels,
glama: mockModels,
unbound: mockModels,
roo: mockModels,
litellm: mockModels,
ollama: {},
lmstudio: {},
@ -2742,6 +2750,7 @@ describe("ClineProvider - Router Models", () => {
.mockRejectedValueOnce(new Error("Unbound API error")) // unbound fail
.mockResolvedValueOnce(mockModels) // vercel-ai-gateway success
.mockResolvedValueOnce(mockModels) // deepinfra success
.mockResolvedValueOnce(mockModels) // roo success
.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail
await messageHandler({ type: "requestRouterModels" })
@ -2755,6 +2764,7 @@ describe("ClineProvider - Router Models", () => {
requesty: {},
glama: mockModels,
unbound: {},
roo: mockModels,
ollama: {},
lmstudio: {},
litellm: {},
@ -2869,6 +2879,7 @@ describe("ClineProvider - Router Models", () => {
requesty: mockModels,
glama: mockModels,
unbound: mockModels,
roo: mockModels,
litellm: {},
ollama: {},
lmstudio: {},

View file

@ -218,12 +218,18 @@ describe("webviewMessageHandler - requestRouterModels", () => {
})
// Verify getModels was called for each provider
expect(mockGetModels).toHaveBeenCalledWith({ provider: "deepinfra" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "openrouter" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "requesty", apiKey: "requesty-key" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "glama" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "unbound", apiKey: "unbound-key" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" })
expect(mockGetModels).toHaveBeenCalledWith({ provider: "deepinfra" })
expect(mockGetModels).toHaveBeenCalledWith(
expect.objectContaining({
provider: "roo",
baseUrl: expect.any(String),
}),
)
expect(mockGetModels).toHaveBeenCalledWith({
provider: "litellm",
apiKey: "litellm-key",
@ -242,6 +248,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
glama: mockModels,
unbound: mockModels,
litellm: mockModels,
roo: mockModels,
ollama: {},
lmstudio: {},
"vercel-ai-gateway": mockModels,
@ -332,6 +339,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
requesty: mockModels,
glama: mockModels,
unbound: mockModels,
roo: mockModels,
litellm: {},
ollama: {},
lmstudio: {},
@ -360,6 +368,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
.mockResolvedValueOnce(mockModels) // vercel-ai-gateway
.mockResolvedValueOnce(mockModels) // deepinfra
.mockResolvedValueOnce(mockModels) // roo
.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
await webviewMessageHandler(mockClineProvider, {
@ -375,6 +384,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
requesty: {},
glama: mockModels,
unbound: {},
roo: mockModels,
litellm: {},
ollama: {},
lmstudio: {},
@ -416,6 +426,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
.mockRejectedValueOnce(new Error("Unbound API error")) // unbound
.mockRejectedValueOnce(new Error("Vercel AI Gateway error")) // vercel-ai-gateway
.mockRejectedValueOnce(new Error("DeepInfra API error")) // deepinfra
.mockRejectedValueOnce(new Error("Roo API error")) // roo
.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
await webviewMessageHandler(mockClineProvider, {
@ -458,6 +469,20 @@ describe("webviewMessageHandler - requestRouterModels", () => {
values: { provider: "deepinfra" },
})
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "singleRouterModelFetchResponse",
success: false,
error: "Vercel AI Gateway error",
values: { provider: "vercel-ai-gateway" },
})
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "singleRouterModelFetchResponse",
success: false,
error: "Roo API error",
values: { provider: "roo" },
})
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "singleRouterModelFetchResponse",
success: false,

View file

@ -769,6 +769,7 @@ export const webviewMessageHandler = async (
glama: {},
ollama: {},
lmstudio: {},
roo: {},
}
const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
@ -805,6 +806,16 @@ export const webviewMessageHandler = async (
baseUrl: apiConfiguration.deepInfraBaseUrl,
},
},
{
key: "roo",
options: {
provider: "roo",
baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy",
apiKey: CloudService.hasInstance()
? CloudService.instance.authService?.getSessionToken()
: undefined,
},
},
]
// Add IO Intelligence if API key is provided.
@ -919,6 +930,38 @@ export const webviewMessageHandler = async (
}
break
}
case "requestRooModels": {
// Specific handler for Roo models only - flushes cache to ensure fresh auth token is used
try {
// Flush cache first to ensure fresh models with current auth state
await flushModels("roo")
const rooModels = await getModels({
provider: "roo",
baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy",
apiKey: CloudService.hasInstance()
? CloudService.instance.authService?.getSessionToken()
: undefined,
})
// Always send a response, even if no models are returned
provider.postMessageToWebview({
type: "singleRouterModelFetchResponse",
success: true,
values: { provider: "roo", models: rooModels },
})
} catch (error) {
// Send error response
const errorMessage = error instanceof Error ? error.message : String(error)
provider.postMessageToWebview({
type: "singleRouterModelFetchResponse",
success: false,
error: errorMessage,
values: { provider: "roo" },
})
}
break
}
case "requestOpenAiModels":
if (message?.values?.baseUrl && message?.values?.apiKey) {
const openAiModels = await getOpenAiModels(

View file

@ -69,6 +69,7 @@ export interface WebviewMessage {
| "requestOpenAiModels"
| "requestOllamaModels"
| "requestLmStudioModels"
| "requestRooModels"
| "requestVsCodeLmModels"
| "requestHuggingFaceModels"
| "openImage"

View file

@ -163,6 +163,7 @@ const dynamicProviderExtras = {
glama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
ollama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
roo: {} as { apiKey?: string; baseUrl?: string },
} as const satisfies Record<RouterName, object>
// Build the dynamic options union from the map, intersected with CommonFetchParams

View file

@ -1,7 +1,7 @@
import React, { memo, useCallback, useEffect, useMemo, useState } from "react"
import { convertHeadersToObject } from "./utils/headers"
import { useDebounce } from "react-use"
import { VSCodeLink, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { ExternalLinkIcon } from "@radix-ui/react-icons"
import {
@ -85,6 +85,7 @@ import {
OpenRouter,
QwenCode,
Requesty,
Roo,
SambaNova,
Unbound,
Vertex,
@ -228,7 +229,11 @@ const ApiOptions = ({
vscode.postMessage({ type: "requestLmStudioModels" })
} else if (selectedProvider === "vscode-lm") {
vscode.postMessage({ type: "requestVsCodeLmModels" })
} else if (selectedProvider === "litellm" || selectedProvider === "deepinfra") {
} else if (
selectedProvider === "litellm" ||
selectedProvider === "deepinfra" ||
selectedProvider === "roo"
) {
vscode.postMessage({ type: "requestRouterModels" })
}
},
@ -667,22 +672,14 @@ const ApiOptions = ({
)}
{selectedProvider === "roo" && (
<div className="flex flex-col gap-3">
{cloudIsAuthenticated ? (
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.roo.authenticatedMessage")}
</div>
) : (
<div className="flex flex-col gap-2">
<VSCodeButton
appearance="primary"
onClick={() => vscode.postMessage({ type: "rooCloudSignIn" })}
className="w-fit">
{t("settings:providers.roo.connectButton")}
</VSCodeButton>
</div>
)}
</div>
<Roo
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
cloudIsAuthenticated={cloudIsAuthenticated}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
/>
)}
{selectedProvider === "featherless" && (

View file

@ -37,6 +37,7 @@ type ModelIdKey = keyof Pick<
| "deepInfraModelId"
| "ioIntelligenceModelId"
| "vercelAiGatewayModelId"
| "apiModelId"
>
interface ModelPickerProps {

View file

@ -19,7 +19,6 @@ import {
doubaoModels,
internationalZAiModels,
fireworksModels,
rooModels,
featherlessModels,
} from "@roo-code/types"
@ -42,7 +41,6 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
sambanova: sambaNovaModels,
zai: internationalZAiModels,
fireworks: fireworksModels,
roo: rooModels,
featherless: featherlessModels,
}

View file

@ -0,0 +1,60 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { type ProviderSettings, type OrganizationAllowList, rooDefaultModelId } from "@roo-code/types"
import type { RouterModels } from "@roo/api"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { vscode } from "@src/utils/vscode"
import { ModelPicker } from "../ModelPicker"
type RooProps = {
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
routerModels?: RouterModels
cloudIsAuthenticated: boolean
organizationAllowList: OrganizationAllowList
modelValidationError?: string
}
export const Roo = ({
apiConfiguration,
setApiConfigurationField,
routerModels,
cloudIsAuthenticated,
organizationAllowList,
modelValidationError,
}: RooProps) => {
const { t } = useAppTranslation()
return (
<>
{cloudIsAuthenticated ? (
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.roo.authenticatedMessage")}
</div>
) : (
<div className="flex flex-col gap-2">
<VSCodeButton
appearance="primary"
onClick={() => vscode.postMessage({ type: "rooCloudSignIn" })}
className="w-fit">
{t("settings:providers.roo.connectButton")}
</VSCodeButton>
</div>
)}
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId={rooDefaultModelId}
models={routerModels?.roo ?? {}}
modelIdKey="apiModelId"
serviceName="Roo Code Cloud"
serviceUrl="https://roocode.com"
organizationAllowList={organizationAllowList}
errorMessage={modelValidationError}
/>
</>
)
}

View file

@ -18,6 +18,7 @@ export { OpenAI } from "./OpenAI"
export { OpenAICompatible } from "./OpenAICompatible"
export { OpenRouter } from "./OpenRouter"
export { QwenCode } from "./QwenCode"
export { Roo } from "./Roo"
export { Requesty } from "./Requesty"
export { SambaNova } from "./SambaNova"
export { Unbound } from "./Unbound"

View file

@ -51,7 +51,6 @@ import {
ioIntelligenceDefaultModelId,
ioIntelligenceModels,
rooDefaultModelId,
rooModels,
qwenCodeDefaultModelId,
qwenCodeModels,
vercelAiGatewayDefaultModelId,
@ -330,21 +329,10 @@ function getSelectedModel({
return { id, info }
}
case "roo": {
const requestedId = apiConfiguration.apiModelId
// Check if the requested model exists in rooModels
if (requestedId && rooModels[requestedId as keyof typeof rooModels]) {
return {
id: requestedId,
info: rooModels[requestedId as keyof typeof rooModels],
}
}
// Fallback to default model if requested model doesn't exist or is not specified
return {
id: rooDefaultModelId,
info: rooModels[rooDefaultModelId as keyof typeof rooModels],
}
// Roo is a dynamic provider - models are loaded from API
const id = apiConfiguration.apiModelId ?? rooDefaultModelId
const info = routerModels.roo[id]
return { id, info }
}
case "qwen-code": {
const id = apiConfiguration.apiModelId ?? qwenCodeDefaultModelId

View file

@ -289,6 +289,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
global: {},
})
const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(true)
const [prevCloudIsAuthenticated, setPrevCloudIsAuthenticated] = useState(false)
const setListApiConfigMeta = useCallback(
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
@ -420,6 +421,16 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
vscode.postMessage({ type: "webviewDidLaunch" })
}, [])
// Watch for authentication state changes and refresh Roo models
useEffect(() => {
const currentAuth = state.cloudIsAuthenticated ?? false
if (!prevCloudIsAuthenticated && currentAuth) {
// User just authenticated - refresh Roo models with the new auth token
vscode.postMessage({ type: "requestRooModels" })
}
setPrevCloudIsAuthenticated(currentAuth)
}, [state.cloudIsAuthenticated, prevCloudIsAuthenticated])
const contextValue: ExtensionStateContextType = {
...state,
reasoningBlockCollapsed: state.reasoningBlockCollapsed ?? true,

View file

@ -43,6 +43,7 @@ describe("Model Validation Functions", () => {
"io-intelligence": {},
"vercel-ai-gateway": {},
huggingface: {},
roo: {},
}
const allowAllOrganization: OrganizationAllowList = {