From 597e9764fb1554694024e2c21f57252a23077ba5 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Date: Fri, 16 May 2025 15:18:35 +0530 Subject: [PATCH 1/4] Adds refresh models button for Unbound provider --- src/api/providers/fetchers/modelCache.ts | 3 +- src/api/providers/fetchers/unbound.ts | 11 +- .../components/settings/providers/Unbound.tsx | 104 +++++++++++++++++- 3 files changed, 114 insertions(+), 4 deletions(-) diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 9ab4b851fc..c3c662415b 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -65,7 +65,8 @@ export const getModels = async ( models = await getGlamaModels() break case "unbound": - models = await getUnboundModels() + // Unbound models endpoint requires an API key to fetch application specific models + models = await getUnboundModels(apiKey) break case "litellm": if (apiKey && baseUrl) { diff --git a/src/api/providers/fetchers/unbound.ts b/src/api/providers/fetchers/unbound.ts index 73a8c2f897..5af9043ba0 100644 --- a/src/api/providers/fetchers/unbound.ts +++ b/src/api/providers/fetchers/unbound.ts @@ -2,11 +2,17 @@ import axios from "axios" import { ModelInfo } from "../../../shared/api" -export async function getUnboundModels(): Promise> { +export async function getUnboundModels(apiKey?: string | null): Promise> { const models: Record = {} try { - const response = await axios.get("https://api.getunbound.ai/models") + const headers: Record = {} + + if (apiKey) { + headers["Authorization"] = `Bearer ${apiKey}` + } + + const response = await axios.get("https://api.getunbound.ai/models", { headers }) if (response.data) { const rawModels: Record = response.data @@ -40,6 +46,7 @@ export async function getUnboundModels(): Promise> { } } catch (error) { console.error(`Error fetching Unbound models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + return {} } return models diff --git a/webview-ui/src/components/settings/providers/Unbound.tsx b/webview-ui/src/components/settings/providers/Unbound.tsx index 77b24bb7cc..acc6f1a668 100644 --- a/webview-ui/src/components/settings/providers/Unbound.tsx +++ b/webview-ui/src/components/settings/providers/Unbound.tsx @@ -1,10 +1,13 @@ -import { useCallback } from "react" +import { useCallback, useState } from "react" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { useQueryClient } from "@tanstack/react-query" import { ProviderSettings, RouterModels, unboundDefaultModelId } from "@roo/shared/api" import { useAppTranslation } from "@src/i18n/TranslationContext" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" +import { vscode } from "@src/utils/vscode" +import { Button } from "@src/components/ui" import { inputEventTransform } from "../transforms" import { ModelPicker } from "../ModelPicker" @@ -17,6 +20,9 @@ type UnboundProps = { export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerModels }: UnboundProps) => { const { t } = useAppTranslation() + const [didRefetch, setDidRefetch] = useState() + const [isInvalidKey, setIsInvalidKey] = useState(false) + const queryClient = useQueryClient() const handleInputChange = useCallback( ( @@ -29,6 +35,80 @@ export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerMode [setApiConfigurationField], ) + const saveConfiguration = useCallback(async () => { + vscode.postMessage({ + type: "upsertApiConfiguration", + text: "default", + apiConfiguration: apiConfiguration, + }) + + const waitForStateUpdate = new Promise((resolve) => { + const messageHandler = (event: MessageEvent) => { + const message = event.data + if (message.type === "state") { + window.removeEventListener("message", messageHandler) + resolve() + } + } + window.addEventListener("message", messageHandler) + }) + + await waitForStateUpdate + }, [apiConfiguration]) + + const requestModels = useCallback(async () => { + vscode.postMessage({ type: "flushRouterModels", text: "unbound" }) + + const modelsPromise = new Promise((resolve) => { + const messageHandler = (event: MessageEvent) => { + const message = event.data + if (message.type === "routerModels") { + window.removeEventListener("message", messageHandler) + resolve() + } + } + window.addEventListener("message", messageHandler) + }) + + vscode.postMessage({ type: "requestRouterModels" }) + + await modelsPromise + + await queryClient.invalidateQueries({ queryKey: ["routerModels"] }) + + // After refreshing models, check if current model is in the updated list + // If not, select the first available model + const updatedModels = queryClient.getQueryData<{ unbound: RouterModels }>(["routerModels"])?.unbound + if (updatedModels && Object.keys(updatedModels).length > 0) { + const currentModelId = apiConfiguration?.unboundModelId + const modelExists = currentModelId && Object.prototype.hasOwnProperty.call(updatedModels, currentModelId) + + if (!currentModelId || !modelExists) { + const firstAvailableModelId = Object.keys(updatedModels)[0] + setApiConfigurationField("unboundModelId", firstAvailableModelId) + } + } + + if (!updatedModels || Object.keys(updatedModels).includes("error")) { + return false + } else { + return true + } + }, [queryClient, apiConfiguration, setApiConfigurationField]) + + const handleRefresh = useCallback(async () => { + await saveConfiguration() + const requestModelsResult = await requestModels() + + if (requestModelsResult) { + setDidRefetch(true) + setTimeout(() => setDidRefetch(false), 2000) + } else { + setIsInvalidKey(true) + setTimeout(() => setIsInvalidKey(false), 2000) + } + }, [saveConfiguration, requestModels]) + return ( <> )} +
+ +
+ {didRefetch && ( +
+ {t("settings:providers.refreshModels.success", { + defaultValue: "Models list updated! You can now select from the latest models.", + })} +
+ )} + {isInvalidKey && ( +
+ {t("settings:providers.invalidApiKey", { + defaultValue: "Invalid API key. Please check your API key and try again.", + })} +
+ )} Date: Fri, 16 May 2025 15:25:29 +0530 Subject: [PATCH 2/4] Adds changeset --- .changeset/seven-kids-return.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/seven-kids-return.md diff --git a/.changeset/seven-kids-return.md b/.changeset/seven-kids-return.md new file mode 100644 index 0000000000..d4da5cbc03 --- /dev/null +++ b/.changeset/seven-kids-return.md @@ -0,0 +1,10 @@ +--- +"roo-cline": minor +--- + +Adds refresh models button for Unbound provider +Adds a button above model picker to refresh models based on the current API Key. + +1. Clicking the refresh button saves the API Key and calls /models endpoint using that. +2. Gets the new models and updates the current model if it is invalid for the given API Key. +3. The refresh button also flushes existing Unbound models and refetches them. From 903a908105cb851e092e5a4ef617a86ec334f640 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Date: Fri, 16 May 2025 15:34:14 +0530 Subject: [PATCH 3/4] Optimizes code to prevent memory leak, add error messages --- src/api/providers/fetchers/unbound.ts | 2 +- .../components/settings/providers/Unbound.tsx | 24 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/api/providers/fetchers/unbound.ts b/src/api/providers/fetchers/unbound.ts index 5af9043ba0..7834debf35 100644 --- a/src/api/providers/fetchers/unbound.ts +++ b/src/api/providers/fetchers/unbound.ts @@ -46,7 +46,7 @@ export async function getUnboundModels(apiKey?: string | null): Promise(false) const queryClient = useQueryClient() + // Add refs to store timer IDs + const didRefetchTimerRef = useRef() + const invalidKeyTimerRef = useRef() + const handleInputChange = useCallback( ( field: K, @@ -42,10 +46,16 @@ export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerMode apiConfiguration: apiConfiguration, }) - const waitForStateUpdate = new Promise((resolve) => { + const waitForStateUpdate = new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + window.removeEventListener("message", messageHandler) + reject(new Error("Timeout waiting for state update")) + }, 10000) // 10 second timeout + const messageHandler = (event: MessageEvent) => { const message = event.data if (message.type === "state") { + clearTimeout(timeoutId) window.removeEventListener("message", messageHandler) resolve() } @@ -53,7 +63,11 @@ export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerMode window.addEventListener("message", messageHandler) }) - await waitForStateUpdate + try { + await waitForStateUpdate + } catch (error) { + console.error("Failed to save configuration:", error) + } }, [apiConfiguration]) const requestModels = useCallback(async () => { @@ -102,10 +116,10 @@ export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerMode if (requestModelsResult) { setDidRefetch(true) - setTimeout(() => setDidRefetch(false), 2000) + didRefetchTimerRef.current = setTimeout(() => setDidRefetch(false), 3000) } else { setIsInvalidKey(true) - setTimeout(() => setIsInvalidKey(false), 2000) + invalidKeyTimerRef.current = setTimeout(() => setIsInvalidKey(false), 3000) } }, [saveConfiguration, requestModels]) From aeda5dc1483618d77f25d2c4498e819655d1fa73 Mon Sep 17 00:00:00 2001 From: Pugazhendhi Date: Sat, 17 May 2025 16:13:25 +0530 Subject: [PATCH 4/4] Adds unbound messages to all supported languages --- webview-ui/src/components/settings/providers/Unbound.tsx | 8 ++------ webview-ui/src/i18n/locales/ca/settings.json | 2 ++ webview-ui/src/i18n/locales/de/settings.json | 2 ++ webview-ui/src/i18n/locales/en/settings.json | 2 ++ webview-ui/src/i18n/locales/es/settings.json | 2 ++ webview-ui/src/i18n/locales/fr/settings.json | 2 ++ webview-ui/src/i18n/locales/hi/settings.json | 2 ++ webview-ui/src/i18n/locales/it/settings.json | 2 ++ webview-ui/src/i18n/locales/ja/settings.json | 2 ++ webview-ui/src/i18n/locales/ko/settings.json | 2 ++ webview-ui/src/i18n/locales/nl/settings.json | 2 ++ webview-ui/src/i18n/locales/pl/settings.json | 2 ++ webview-ui/src/i18n/locales/pt-BR/settings.json | 2 ++ webview-ui/src/i18n/locales/ru/settings.json | 2 ++ webview-ui/src/i18n/locales/tr/settings.json | 2 ++ webview-ui/src/i18n/locales/vi/settings.json | 2 ++ webview-ui/src/i18n/locales/zh-CN/settings.json | 2 ++ webview-ui/src/i18n/locales/zh-TW/settings.json | 2 ++ 18 files changed, 36 insertions(+), 6 deletions(-) diff --git a/webview-ui/src/components/settings/providers/Unbound.tsx b/webview-ui/src/components/settings/providers/Unbound.tsx index 6bd79fb119..3d5aa0c67a 100644 --- a/webview-ui/src/components/settings/providers/Unbound.tsx +++ b/webview-ui/src/components/settings/providers/Unbound.tsx @@ -151,16 +151,12 @@ export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerMode {didRefetch && (
- {t("settings:providers.refreshModels.success", { - defaultValue: "Models list updated! You can now select from the latest models.", - })} + {t("settings:providers.unboundRefreshModelsSuccess")}
)} {isInvalidKey && (
- {t("settings:providers.invalidApiKey", { - defaultValue: "Invalid API key. Please check your API key and try again.", - })} + {t("settings:providers.unboundInvalidApiKey")}
)}