Skip to content

Commit 4444328

Browse files
committed
feat: add dynamic model loading for Roo Code Cloud provider
1 parent a8f87d2 commit 4444328

File tree

8 files changed

+164
-8
lines changed

8 files changed

+164
-8
lines changed

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const dynamicProviders = [
4949
"requesty",
5050
"unbound",
5151
"glama",
52+
"roo",
5253
] as const
5354

5455
export type DynamicProvider = (typeof dynamicProviders)[number]

pr-body.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
## Summary
2+
3+
Enables the Roo Code extension to dynamically load available models from the Roo Code Cloud provider via the `/v1/models` endpoint.
4+
5+
## Changes
6+
7+
- **New fetcher**: Added `getRooModels()` function to fetch models from Roo Code Cloud `/v1/models` endpoint
8+
- **Dynamic provider**: Added "roo" to the list of dynamic providers
9+
- **Type updates**: Updated RooHandler to support dynamic model IDs (changed from `RooModelId` to `string`)
10+
- **Model caching**: Integrated with existing modelCache infrastructure for efficient caching
11+
- **Graceful fallback**: Falls back to static model definitions if dynamic loading fails
12+
13+
## Technical Details
14+
15+
### Model Loading Strategy
16+
17+
- Models are loaded asynchronously on handler initialization
18+
- Dynamic models are merged with static models (static definitions take precedence)
19+
- Uses 5-minute memory cache + file cache from existing infrastructure
20+
- 10-second timeout prevents hanging on network issues
21+
22+
### Type Safety
23+
24+
- Maintains backward compatibility with existing static models
25+
- Generic type changed from `RooModelId` to `string` to support dynamic model IDs
26+
- All type definitions updated across shared/api.ts and provider-settings.ts
27+
28+
## Testing
29+
30+
- Linting passes
31+
- Type checks pass
32+
- Follows patterns from other dynamic providers (requesty, glama, unbound)
33+
- Error handling with descriptive logging
34+
35+
## Related
36+
37+
This PR works in conjunction with Roo-Code-Cloud PR #1316 which adds the `/v1/models` endpoint.

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+
models = await getRooModels(
106+
options.baseUrl ?? process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy",
107+
options.apiKey,
108+
)
109+
break
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: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import axios from "axios"
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+
// Normalize the URL to ensure proper /v1/models endpoint construction
27+
const urlObj = new URL(baseUrl)
28+
urlObj.pathname = urlObj.pathname.replace(/\/+$/, "").replace(/\/+/g, "/") + "/v1/models"
29+
const url = urlObj.href
30+
31+
// Added timeout to prevent indefinite hanging
32+
const response = await axios.get(url, { headers, timeout: 10000 })
33+
const models: ModelRecord = {}
34+
35+
// Process the model info from the response
36+
// Expected format: { object: "list", data: [{ id: string, object: "model", created: number, owned_by: string }] }
37+
if (response.data && response.data.data && Array.isArray(response.data.data)) {
38+
for (const model of response.data.data) {
39+
const modelId = model.id
40+
41+
if (!modelId) continue
42+
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
46+
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`,
54+
}
55+
}
56+
} else {
57+
// If response.data.data is not in the expected format, consider it an error.
58+
console.error("Error fetching Roo Code Cloud models: Unexpected response format", response.data)
59+
throw new Error("Failed to fetch Roo Code Cloud models: Unexpected response format.")
60+
}
61+
62+
return models
63+
} catch (error: any) {
64+
console.error("Error fetching Roo Code Cloud models:", error.message ? error.message : error)
65+
if (axios.isAxiosError(error) && error.response) {
66+
throw new Error(
67+
`Failed to fetch Roo Code Cloud models: ${error.response.status} ${error.response.statusText}. Check base URL and API key.`,
68+
)
69+
} else if (axios.isAxiosError(error) && error.request) {
70+
throw new Error(
71+
"Failed to fetch Roo Code Cloud models: No response from server. Check Roo Code Cloud server status and base URL.",
72+
)
73+
} else {
74+
throw new Error(`Failed to fetch Roo Code Cloud models: ${error.message || "An unknown error occurred."}`)
75+
}
76+
}
77+
}

src/api/providers/roo.ts

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

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

7-
import type { ApiHandlerOptions } from "../../shared/api"
7+
import type { ApiHandlerOptions, ModelRecord } from "../../shared/api"
88
import { ApiStream } from "../transform/stream"
99

1010
import type { ApiHandlerCreateMessageMetadata } from "../index"
1111
import { DEFAULT_HEADERS } from "./constants"
1212
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
13+
import { getModels } from "../providers/fetchers/modelCache"
1314

14-
export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
15+
export class RooHandler extends BaseOpenAiCompatibleProvider<string> {
1516
private authStateListener?: (state: { state: AuthState }) => void
17+
private mergedModels: Record<string, ModelInfo> = rooModels as Record<string, ModelInfo>
18+
private modelsLoaded = false
1619

1720
constructor(options: ApiHandlerOptions) {
1821
let sessionToken: string | undefined = undefined
@@ -21,18 +24,25 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
2124
sessionToken = CloudService.instance.authService?.getSessionToken()
2225
}
2326

27+
const baseURL = process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy"
28+
2429
// Always construct the handler, even without a valid token.
2530
// The provider-proxy server will return 401 if authentication fails.
2631
super({
2732
...options,
2833
providerName: "Roo Code Cloud",
29-
baseURL: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy/v1",
34+
baseURL,
3035
apiKey: sessionToken || "unauthenticated", // Use a placeholder if no token.
3136
defaultProviderModelId: rooDefaultModelId,
32-
providerModels: rooModels,
37+
providerModels: rooModels as Record<string, ModelInfo>,
3338
defaultTemperature: 0.7,
3439
})
3540

41+
// Load dynamic models asynchronously
42+
this.loadDynamicModels(baseURL, sessionToken).catch((error) => {
43+
console.error("[RooHandler] Failed to load dynamic models:", error)
44+
})
45+
3646
if (CloudService.hasInstance()) {
3747
const cloudService = CloudService.instance
3848

@@ -103,17 +113,37 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
103113
}
104114
}
105115

116+
private async loadDynamicModels(baseURL: string, apiKey?: string): Promise<void> {
117+
try {
118+
const dynamicModels = await getModels({
119+
provider: "roo",
120+
baseUrl: baseURL,
121+
apiKey,
122+
})
123+
this.modelsLoaded = true
124+
125+
// Merge dynamic models with static models, preferring static model info
126+
this.mergedModels = { ...dynamicModels, ...rooModels } as Record<string, ModelInfo>
127+
} catch (error) {
128+
console.error("[RooHandler] Error loading dynamic models:", error)
129+
// Keep using static models as fallback
130+
this.modelsLoaded = false
131+
}
132+
}
133+
106134
override getModel() {
107135
const modelId = this.options.apiModelId || rooDefaultModelId
108-
const modelInfo = this.providerModels[modelId as RooModelId] ?? this.providerModels[rooDefaultModelId]
136+
137+
// Try to find the model in the merged models (which includes both static and dynamic)
138+
const modelInfo = this.mergedModels[modelId]
109139

110140
if (modelInfo) {
111-
return { id: modelId as RooModelId, info: modelInfo }
141+
return { id: modelId, info: modelInfo }
112142
}
113143

114144
// Return the requested model ID even if not found, with fallback info.
115145
return {
116-
id: modelId as RooModelId,
146+
id: modelId,
117147
info: {
118148
maxTokens: 16_384,
119149
contextWindow: 262_144,

src/core/webview/webviewMessageHandler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,7 @@ export const webviewMessageHandler = async (
768768
glama: {},
769769
ollama: {},
770770
lmstudio: {},
771+
roo: {},
771772
}
772773

773774
const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {

src/shared/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ const dynamicProviderExtras = {
163163
glama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
164164
ollama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
165165
lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
166+
roo: {} as { apiKey?: string; baseUrl?: string },
166167
} as const satisfies Record<RouterName, object>
167168

168169
// Build the dynamic options union from the map, intersected with CommonFetchParams

webview-ui/src/utils/__tests__/validate.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ describe("Model Validation Functions", () => {
4343
"io-intelligence": {},
4444
"vercel-ai-gateway": {},
4545
huggingface: {},
46+
roo: {},
4647
}
4748

4849
const allowAllOrganization: OrganizationAllowList = {

0 commit comments

Comments
 (0)