Skip to content

Commit cd02cd8

Browse files
committed
Merge branch 'main' into litellm-provider-config, revert modelCache logic changes
2 parents 2e8160b + d70d0c1 commit cd02cd8

File tree

28 files changed

+279
-31
lines changed

28 files changed

+279
-31
lines changed

.changeset/curly-plants-pull.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
New models for the Chutes provider:
6+
7+
- Qwen/Qwen3-235B-A22B
8+
- Qwen/Qwen3-32B
9+
- Qwen/Qwen3-30B-A3B
10+
- Qwen/Qwen3-14B
11+
- Qwen/Qwen3-8B

.changeset/seven-kids-return.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"roo-cline": minor
3+
---
4+
5+
Adds refresh models button for Unbound provider
6+
Adds a button above model picker to refresh models based on the current API Key.
7+
8+
1. Clicking the refresh button saves the API Key and calls /models endpoint using that.
9+
2. Gets the new models and updates the current model if it is invalid for the given API Key.
10+
3. The refresh button also flushes existing Unbound models and refetches them.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,7 @@ logs
3636

3737
# Vite development
3838
.vite-port
39+
40+
# IntelliJ and Qodo plugin folders
41+
.idea/
42+
.qodo/

.vscodeignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,7 @@ assets/docs/**
6868

6969
# Include .env file for telemetry
7070
!.env
71+
72+
# Ignore IntelliJ and Qodo plugin folders
73+
.idea/**
74+
.qodo/**

esbuild.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,17 @@ const copyWasmFiles = {
3434

3535
// tiktoken WASM file
3636
fs.copyFileSync(
37-
path.join(nodeModulesDir, "tiktoken", "tiktoken_bg.wasm"),
37+
path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
3838
path.join(distDir, "tiktoken_bg.wasm"),
3939
)
4040

41+
// Also copy to the workers directory
42+
fs.mkdirSync(path.join(distDir, "workers"), { recursive: true })
43+
fs.copyFileSync(
44+
path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
45+
path.join(distDir, "workers", "tiktoken_bg.wasm"),
46+
)
47+
4148
// Main tree-sitter WASM file
4249
fs.copyFileSync(
4350
path.join(nodeModulesDir, "web-tree-sitter", "tree-sitter.wasm"),

src/api/providers/fetchers/modelCache.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import NodeCache from "node-cache"
66
import { ContextProxy } from "../../../core/config/ContextProxy"
77
import { getCacheDirectoryPath } from "../../../shared/storagePathManager"
88
import { RouterName, ModelRecord } from "../../../shared/api"
9+
import { fileExistsAtPath } from "../../../utils/fs"
910

1011
import { getOpenRouterModels } from "./openrouter"
1112
import { getRequestyModels } from "./requesty"
@@ -21,6 +22,14 @@ async function writeModels(router: RouterName, data: ModelRecord) {
2122
await fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data))
2223
}
2324

25+
async function readModels(router: RouterName): Promise<ModelRecord | undefined> {
26+
const filename = `${router}_models.json`
27+
const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath)
28+
const filePath = path.join(cacheDir, filename)
29+
const exists = await fileExistsAtPath(filePath)
30+
return exists ? JSON.parse(await fs.readFile(filePath, "utf8")) : undefined
31+
}
32+
2433
/**
2534
* Get models from the cache or fetch them from the provider and cache them.
2635
* There are two caches:
@@ -37,37 +46,34 @@ export const getModels = async (
3746
apiKey: string | undefined = undefined,
3847
baseUrl: string | undefined = undefined,
3948
): Promise<ModelRecord> => {
40-
// If this call is meant for a refresh (indicated by apiKey/baseUrl for specific routers),
41-
// the memory cache should have been flushed by the caller (e.g., webviewMessageHandler).
42-
// Otherwise, for general calls, check memory cache first.
43-
const modelsFromMemory = memoryCache.get<ModelRecord>(router)
44-
if (modelsFromMemory) {
45-
return modelsFromMemory
49+
let models = memoryCache.get<ModelRecord>(router)
50+
if (models) {
51+
return models
4652
}
4753

48-
let fetchedModels: ModelRecord
4954
try {
5055
switch (router) {
5156
case "openrouter":
52-
fetchedModels = await getOpenRouterModels()
57+
models = await getOpenRouterModels()
5358
break
5459
case "requesty":
55-
// Assuming getRequestyModels will throw if apiKey is needed and not provided or invalid.
56-
fetchedModels = await getRequestyModels(apiKey)
60+
// Requesty models endpoint requires an API key for per-user custom policies
61+
models = await getRequestyModels(apiKey)
5762
break
5863
case "glama":
59-
fetchedModels = await getGlamaModels()
64+
models = await getGlamaModels()
6065
break
6166
case "unbound":
62-
fetchedModels = await getUnboundModels()
67+
// Unbound models endpoint requires an API key to fetch application specific models
68+
models = await getUnboundModels(apiKey)
6369
break
6470
case "litellm":
6571
if (!baseUrl || !apiKey) {
6672
// This case should ideally be handled by the caller if baseUrl is strictly required.
6773
// However, for robustness, if called without baseUrl for litellm, it would fail in getLiteLLMModels or here.
6874
throw new Error("Base URL and api key are required for LiteLLM models.")
6975
}
70-
fetchedModels = await getLiteLLMModels(apiKey || "", baseUrl)
76+
models = await getLiteLLMModels(apiKey || "", baseUrl)
7177
break
7278
default:
7379
// Ensures router is exhaustively checked if RouterName is a strict union
@@ -76,11 +82,18 @@ export const getModels = async (
7682
}
7783

7884
// Cache the fetched models (even if empty, to signify a successful fetch with no models)
79-
memoryCache.set(router, fetchedModels)
80-
await writeModels(router, fetchedModels).catch((err) =>
85+
memoryCache.set(router, models)
86+
await writeModels(router, models).catch((err) =>
8187
console.error(`[getModels] Error writing ${router} models to file cache:`, err),
8288
)
83-
return fetchedModels
89+
90+
try {
91+
models = await readModels(router)
92+
// console.log(`[getModels] read ${router} models from file cache`)
93+
} catch (error) {
94+
console.error(`[getModels] error reading ${router} models from file cache`, error)
95+
}
96+
return models || {}
8497
} catch (error) {
8598
// Log the error and re-throw it so the caller can handle it (e.g., show a UI message).
8699
console.error(`[getModels] Failed to fetch models for ${router}:`, error)
@@ -89,10 +102,6 @@ export const getModels = async (
89102
}
90103
}
91104

92-
/**
93-
* Flush models memory cache for a specific router
94-
* @param router - The router to flush models for.
95-
*/
96105
export const flushModels = async (router: RouterName) => {
97106
memoryCache.del(router)
98107
}

src/api/providers/fetchers/unbound.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ import axios from "axios"
22

33
import { ModelInfo } from "../../../shared/api"
44

5-
export async function getUnboundModels(): Promise<Record<string, ModelInfo>> {
5+
export async function getUnboundModels(apiKey?: string | null): Promise<Record<string, ModelInfo>> {
66
const models: Record<string, ModelInfo> = {}
77

88
try {
9-
const response = await axios.get("https://api.getunbound.ai/models")
9+
const headers: Record<string, string> = {}
10+
11+
if (apiKey) {
12+
headers["Authorization"] = `Bearer ${apiKey}`
13+
}
14+
15+
const response = await axios.get("https://api.getunbound.ai/models", { headers })
1016

1117
if (response.data) {
1218
const rawModels: Record<string, any> = response.data
@@ -40,6 +46,7 @@ export async function getUnboundModels(): Promise<Record<string, ModelInfo>> {
4046
}
4147
} catch (error) {
4248
console.error(`Error fetching Unbound models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
49+
throw new Error(`Failed to fetch Unbound models: ${error instanceof Error ? error.message : "Unknown error"}`)
4350
}
4451

4552
return models

src/core/task/Task.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ export class Task extends EventEmitter<ClineEvents> {
350350

351351
await this.providerRef.deref()?.updateTaskHistory(historyItem)
352352
} catch (error) {
353-
console.error("Failed to save cline messages:", error)
353+
console.error("Failed to save Roo messages:", error)
354354
}
355355
}
356356

@@ -372,7 +372,7 @@ export class Task extends EventEmitter<ClineEvents> {
372372
// simply removes the reference to this instance, but the instance is
373373
// still alive until this promise resolves or rejects.)
374374
if (this.abort) {
375-
throw new Error(`[Cline#ask] task ${this.taskId}.${this.instanceId} aborted`)
375+
throw new Error(`[RooCode#ask] task ${this.taskId}.${this.instanceId} aborted`)
376376
}
377377

378378
let askTs: number
@@ -492,7 +492,7 @@ export class Task extends EventEmitter<ClineEvents> {
492492
} = {},
493493
): Promise<undefined> {
494494
if (this.abort) {
495-
throw new Error(`[Cline#say] task ${this.taskId}.${this.instanceId} aborted`)
495+
throw new Error(`[RooCode#say] task ${this.taskId}.${this.instanceId} aborted`)
496496
}
497497

498498
if (partial !== undefined) {
@@ -623,7 +623,7 @@ export class Task extends EventEmitter<ClineEvents> {
623623
} catch (error) {
624624
this.providerRef
625625
.deref()
626-
?.log(`Error failed to add reply from subtast into conversation of parent task, error: ${error}`)
626+
?.log(`Error failed to add reply from subtask into conversation of parent task, error: ${error}`)
627627

628628
throw error
629629
}
@@ -957,7 +957,7 @@ export class Task extends EventEmitter<ClineEvents> {
957957
includeFileDetails: boolean = false,
958958
): Promise<boolean> {
959959
if (this.abort) {
960-
throw new Error(`[Cline#recursivelyMakeClineRequests] task ${this.taskId}.${this.instanceId} aborted`)
960+
throw new Error(`[RooCode#recursivelyMakeRooRequests] task ${this.taskId}.${this.instanceId} aborted`)
961961
}
962962

963963
if (this.consecutiveMistakeCount >= this.consecutiveMistakeLimit) {
@@ -1253,7 +1253,7 @@ export class Task extends EventEmitter<ClineEvents> {
12531253

12541254
// Need to call here in case the stream was aborted.
12551255
if (this.abort || this.abandoned) {
1256-
throw new Error(`[Cline#recursivelyMakeClineRequests] task ${this.taskId}.${this.instanceId} aborted`)
1256+
throw new Error(`[RooCode#recursivelyMakeRooRequests] task ${this.taskId}.${this.instanceId} aborted`)
12571257
}
12581258

12591259
this.didCompleteReadingStream = true

src/shared/api.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,6 +1533,11 @@ export type ChutesModelId =
15331533
| "deepseek-ai/DeepSeek-V3-Base"
15341534
| "deepseek-ai/DeepSeek-R1-Zero"
15351535
| "deepseek-ai/DeepSeek-V3-0324"
1536+
| "Qwen/Qwen3-235B-A22B"
1537+
| "Qwen/Qwen3-32B"
1538+
| "Qwen/Qwen3-30B-A3B"
1539+
| "Qwen/Qwen3-14B"
1540+
| "Qwen/Qwen3-8B"
15361541
| "microsoft/MAI-DS-R1-FP8"
15371542
| "tngtech/DeepSeek-R1T-Chimera"
15381543
export const chutesDefaultModelId: ChutesModelId = "deepseek-ai/DeepSeek-R1"
@@ -1663,6 +1668,51 @@ export const chutesModels = {
16631668
outputPrice: 0,
16641669
description: "DeepSeek V3 (0324) model.",
16651670
},
1671+
"Qwen/Qwen3-235B-A22B": {
1672+
maxTokens: 32768,
1673+
contextWindow: 40960,
1674+
supportsImages: false,
1675+
supportsPromptCache: false,
1676+
inputPrice: 0,
1677+
outputPrice: 0,
1678+
description: "Qwen3 235B A22B model.",
1679+
},
1680+
"Qwen/Qwen3-32B": {
1681+
maxTokens: 32768,
1682+
contextWindow: 40960,
1683+
supportsImages: false,
1684+
supportsPromptCache: false,
1685+
inputPrice: 0,
1686+
outputPrice: 0,
1687+
description: "Qwen3 32B model.",
1688+
},
1689+
"Qwen/Qwen3-30B-A3B": {
1690+
maxTokens: 32768,
1691+
contextWindow: 40960,
1692+
supportsImages: false,
1693+
supportsPromptCache: false,
1694+
inputPrice: 0,
1695+
outputPrice: 0,
1696+
description: "Qwen3 30B A3B model.",
1697+
},
1698+
"Qwen/Qwen3-14B": {
1699+
maxTokens: 32768,
1700+
contextWindow: 40960,
1701+
supportsImages: false,
1702+
supportsPromptCache: false,
1703+
inputPrice: 0,
1704+
outputPrice: 0,
1705+
description: "Qwen3 14B model.",
1706+
},
1707+
"Qwen/Qwen3-8B": {
1708+
maxTokens: 32768,
1709+
contextWindow: 40960,
1710+
supportsImages: false,
1711+
supportsPromptCache: false,
1712+
inputPrice: 0,
1713+
outputPrice: 0,
1714+
description: "Qwen3 8B model.",
1715+
},
16661716
"microsoft/MAI-DS-R1-FP8": {
16671717
maxTokens: 32768,
16681718
contextWindow: 163840,

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@
2121
"useUnknownInCatchVariables": false
2222
},
2323
"include": ["src/**/*", "scripts/**/*", ".changeset/**/*"],
24-
"exclude": ["node_modules", ".vscode-test", "webview-ui"]
24+
"exclude": ["node_modules", ".vscode-test", "webview-ui", ".idea", ".qodo"]
2525
}

0 commit comments

Comments
 (0)