Skip to content

Commit d851586

Browse files
Adds refresh models button for Unbound provider (#3663)
* Adds refresh models button for Unbound provider * Adds changeset * Optimizes code to prevent memory leak, add error messages * Adds unbound messages to all supported languages --------- Co-authored-by: Pugazhendhi <[email protected]>
1 parent 4550a91 commit d851586

File tree

21 files changed

+168
-4
lines changed

21 files changed

+168
-4
lines changed

.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.

src/api/providers/fetchers/modelCache.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ export const getModels = async (
6565
models = await getGlamaModels()
6666
break
6767
case "unbound":
68-
models = await getUnboundModels()
68+
// Unbound models endpoint requires an API key to fetch application specific models
69+
models = await getUnboundModels(apiKey)
6970
break
7071
case "litellm":
7172
if (apiKey && baseUrl) {

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

webview-ui/src/components/settings/providers/Unbound.tsx

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { useCallback } from "react"
1+
import { useCallback, useState, useRef } from "react"
22
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
3+
import { useQueryClient } from "@tanstack/react-query"
34

45
import { ProviderSettings, RouterModels, unboundDefaultModelId } from "@roo/shared/api"
56

67
import { useAppTranslation } from "@src/i18n/TranslationContext"
78
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
9+
import { vscode } from "@src/utils/vscode"
10+
import { Button } from "@src/components/ui"
811

912
import { inputEventTransform } from "../transforms"
1013
import { ModelPicker } from "../ModelPicker"
@@ -17,6 +20,13 @@ type UnboundProps = {
1720

1821
export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerModels }: UnboundProps) => {
1922
const { t } = useAppTranslation()
23+
const [didRefetch, setDidRefetch] = useState<boolean>()
24+
const [isInvalidKey, setIsInvalidKey] = useState<boolean>(false)
25+
const queryClient = useQueryClient()
26+
27+
// Add refs to store timer IDs
28+
const didRefetchTimerRef = useRef<NodeJS.Timeout>()
29+
const invalidKeyTimerRef = useRef<NodeJS.Timeout>()
2030

2131
const handleInputChange = useCallback(
2232
<K extends keyof ProviderSettings, E>(
@@ -29,6 +39,90 @@ export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerMode
2939
[setApiConfigurationField],
3040
)
3141

42+
const saveConfiguration = useCallback(async () => {
43+
vscode.postMessage({
44+
type: "upsertApiConfiguration",
45+
text: "default",
46+
apiConfiguration: apiConfiguration,
47+
})
48+
49+
const waitForStateUpdate = new Promise<void>((resolve, reject) => {
50+
const timeoutId = setTimeout(() => {
51+
window.removeEventListener("message", messageHandler)
52+
reject(new Error("Timeout waiting for state update"))
53+
}, 10000) // 10 second timeout
54+
55+
const messageHandler = (event: MessageEvent) => {
56+
const message = event.data
57+
if (message.type === "state") {
58+
clearTimeout(timeoutId)
59+
window.removeEventListener("message", messageHandler)
60+
resolve()
61+
}
62+
}
63+
window.addEventListener("message", messageHandler)
64+
})
65+
66+
try {
67+
await waitForStateUpdate
68+
} catch (error) {
69+
console.error("Failed to save configuration:", error)
70+
}
71+
}, [apiConfiguration])
72+
73+
const requestModels = useCallback(async () => {
74+
vscode.postMessage({ type: "flushRouterModels", text: "unbound" })
75+
76+
const modelsPromise = new Promise<void>((resolve) => {
77+
const messageHandler = (event: MessageEvent) => {
78+
const message = event.data
79+
if (message.type === "routerModels") {
80+
window.removeEventListener("message", messageHandler)
81+
resolve()
82+
}
83+
}
84+
window.addEventListener("message", messageHandler)
85+
})
86+
87+
vscode.postMessage({ type: "requestRouterModels" })
88+
89+
await modelsPromise
90+
91+
await queryClient.invalidateQueries({ queryKey: ["routerModels"] })
92+
93+
// After refreshing models, check if current model is in the updated list
94+
// If not, select the first available model
95+
const updatedModels = queryClient.getQueryData<{ unbound: RouterModels }>(["routerModels"])?.unbound
96+
if (updatedModels && Object.keys(updatedModels).length > 0) {
97+
const currentModelId = apiConfiguration?.unboundModelId
98+
const modelExists = currentModelId && Object.prototype.hasOwnProperty.call(updatedModels, currentModelId)
99+
100+
if (!currentModelId || !modelExists) {
101+
const firstAvailableModelId = Object.keys(updatedModels)[0]
102+
setApiConfigurationField("unboundModelId", firstAvailableModelId)
103+
}
104+
}
105+
106+
if (!updatedModels || Object.keys(updatedModels).includes("error")) {
107+
return false
108+
} else {
109+
return true
110+
}
111+
}, [queryClient, apiConfiguration, setApiConfigurationField])
112+
113+
const handleRefresh = useCallback(async () => {
114+
await saveConfiguration()
115+
const requestModelsResult = await requestModels()
116+
117+
if (requestModelsResult) {
118+
setDidRefetch(true)
119+
didRefetchTimerRef.current = setTimeout(() => setDidRefetch(false), 3000)
120+
} else {
121+
setIsInvalidKey(true)
122+
invalidKeyTimerRef.current = setTimeout(() => setIsInvalidKey(false), 3000)
123+
}
124+
}, [saveConfiguration, requestModels])
125+
32126
return (
33127
<>
34128
<VSCodeTextField
@@ -47,6 +141,24 @@ export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerMode
47141
{t("settings:providers.getUnboundApiKey")}
48142
</VSCodeButtonLink>
49143
)}
144+
<div className="flex justify-end">
145+
<Button variant="outline" onClick={handleRefresh} className="w-1/2 max-w-xs">
146+
<div className="flex items-center gap-2 justify-center">
147+
<span className="codicon codicon-refresh" />
148+
{t("settings:providers.refreshModels.label")}
149+
</div>
150+
</Button>
151+
</div>
152+
{didRefetch && (
153+
<div className="flex items-center text-vscode-charts-green">
154+
{t("settings:providers.unboundRefreshModelsSuccess")}
155+
</div>
156+
)}
157+
{isInvalidKey && (
158+
<div className="flex items-center text-vscode-errorForeground">
159+
{t("settings:providers.unboundInvalidApiKey")}
160+
</div>
161+
)}
50162
<ModelPicker
51163
apiConfiguration={apiConfiguration}
52164
defaultModelId={unboundDefaultModelId}

webview-ui/src/i18n/locales/ca/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@
185185
},
186186
"unboundApiKey": "Clau API d'Unbound",
187187
"getUnboundApiKey": "Obtenir clau API d'Unbound",
188+
"unboundRefreshModelsSuccess": "Llista de models actualitzada! Ara podeu seleccionar entre els últims models.",
189+
"unboundInvalidApiKey": "Clau API no vàlida. Si us plau, comproveu la vostra clau API i torneu-ho a provar.",
188190
"humanRelay": {
189191
"description": "No es requereix clau API, però l'usuari necessita ajuda per copiar i enganxar informació al xat d'IA web.",
190192
"instructions": "Durant l'ús, apareixerà un diàleg i el missatge actual es copiarà automàticament al porta-retalls. Necessiteu enganxar-lo a les versions web d'IA (com ChatGPT o Claude), després copiar la resposta de l'IA de nou al diàleg i fer clic al botó de confirmació."

webview-ui/src/i18n/locales/de/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@
185185
},
186186
"unboundApiKey": "Unbound API-Schlüssel",
187187
"getUnboundApiKey": "Unbound API-Schlüssel erhalten",
188+
"unboundRefreshModelsSuccess": "Modellliste aktualisiert! Sie können jetzt aus den neuesten Modellen auswählen.",
189+
"unboundInvalidApiKey": "Ungültiger API-Schlüssel. Bitte überprüfen Sie Ihren API-Schlüssel und versuchen Sie es erneut.",
188190
"humanRelay": {
189191
"description": "Es ist kein API-Schlüssel erforderlich, aber der Benutzer muss beim Kopieren und Einfügen der Informationen in den Web-Chat-KI helfen.",
190192
"instructions": "Während der Verwendung wird ein Dialogfeld angezeigt und die aktuelle Nachricht wird automatisch in die Zwischenablage kopiert. Du musst diese in Web-Versionen von KI (wie ChatGPT oder Claude) einfügen, dann die Antwort der KI zurück in das Dialogfeld kopieren und auf die Bestätigungsschaltfläche klicken."

webview-ui/src/i18n/locales/en/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@
185185
},
186186
"unboundApiKey": "Unbound API Key",
187187
"getUnboundApiKey": "Get Unbound API Key",
188+
"unboundRefreshModelsSuccess": "Models list updated! You can now select from the latest models.",
189+
"unboundInvalidApiKey": "Invalid API key. Please check your API key and try again.",
188190
"humanRelay": {
189191
"description": "No API key is required, but the user needs to help copy and paste the information to the web chat AI.",
190192
"instructions": "During use, a dialog box will pop up and the current message will be copied to the clipboard automatically. You need to paste these to web versions of AI (such as ChatGPT or Claude), then copy the AI's reply back to the dialog box and click the confirm button."

webview-ui/src/i18n/locales/es/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@
185185
},
186186
"unboundApiKey": "Clave API de Unbound",
187187
"getUnboundApiKey": "Obtener clave API de Unbound",
188+
"unboundRefreshModelsSuccess": "¡Lista de modelos actualizada! Ahora puede seleccionar entre los últimos modelos.",
189+
"unboundInvalidApiKey": "Clave API inválida. Por favor, verifique su clave API e inténtelo de nuevo.",
188190
"humanRelay": {
189191
"description": "No se requiere clave API, pero el usuario necesita ayudar a copiar y pegar la información en el chat web de IA.",
190192
"instructions": "Durante el uso, aparecerá un cuadro de diálogo y el mensaje actual se copiará automáticamente al portapapeles. Debe pegarlo en las versiones web de IA (como ChatGPT o Claude), luego copiar la respuesta de la IA de vuelta al cuadro de diálogo y hacer clic en el botón de confirmar."

webview-ui/src/i18n/locales/fr/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@
185185
},
186186
"unboundApiKey": "Clé API Unbound",
187187
"getUnboundApiKey": "Obtenir la clé API Unbound",
188+
"unboundRefreshModelsSuccess": "Liste des modèles mise à jour ! Vous pouvez maintenant sélectionner parmi les derniers modèles.",
189+
"unboundInvalidApiKey": "Clé API invalide. Veuillez vérifier votre clé API et réessayer.",
188190
"humanRelay": {
189191
"description": "Aucune clé API n'est requise, mais l'utilisateur doit aider à copier et coller les informations dans le chat web de l'IA.",
190192
"instructions": "Pendant l'utilisation, une boîte de dialogue apparaîtra et le message actuel sera automatiquement copié dans le presse-papiers. Vous devez le coller dans les versions web de l'IA (comme ChatGPT ou Claude), puis copier la réponse de l'IA dans la boîte de dialogue et cliquer sur le bouton de confirmation."

webview-ui/src/i18n/locales/hi/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@
185185
},
186186
"unboundApiKey": "Unbound API कुंजी",
187187
"getUnboundApiKey": "Unbound API कुंजी प्राप्त करें",
188+
"unboundRefreshModelsSuccess": "मॉडल सूची अपडेट हो गई है! अब आप नवीनतम मॉडलों में से चुन सकते हैं।",
189+
"unboundInvalidApiKey": "अमान्य API कुंजी। कृपया अपनी API कुंजी की जांच करें और पुनः प्रयास करें।",
188190
"humanRelay": {
189191
"description": "कोई API कुंजी आवश्यक नहीं है, लेकिन उपयोगकर्ता को वेब चैट AI में जानकारी कॉपी और पेस्ट करने में मदद करनी होगी।",
190192
"instructions": "उपयोग के दौरान, एक डायलॉग बॉक्स पॉप अप होगा और वर्तमान संदेश स्वचालित रूप से क्लिपबोर्ड पर कॉपी हो जाएगा। आपको इन्हें AI के वेब संस्करणों (जैसे ChatGPT या Claude) में पेस्ट करना होगा, फिर AI की प्रतिक्रिया को डायलॉग बॉक्स में वापस कॉपी करें और पुष्टि बटन पर क्लिक करें।"

0 commit comments

Comments
 (0)