Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/seven-kids-return.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 2 additions & 1 deletion src/api/providers/fetchers/modelCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
11 changes: 9 additions & 2 deletions src/api/providers/fetchers/unbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ import axios from "axios"

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

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

try {
const response = await axios.get("https://api.getunbound.ai/models")
const headers: Record<string, string> = {}

if (apiKey) {
headers["Authorization"] = `Bearer ${apiKey}`
}

const response = await axios.get("https://api.getunbound.ai/models", { headers })

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

return models
Expand Down
114 changes: 113 additions & 1 deletion webview-ui/src/components/settings/providers/Unbound.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useCallback } from "react"
import { useCallback, useState, useRef } 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"
Expand All @@ -17,6 +20,13 @@ type UnboundProps = {

export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerModels }: UnboundProps) => {
const { t } = useAppTranslation()
const [didRefetch, setDidRefetch] = useState<boolean>()
const [isInvalidKey, setIsInvalidKey] = useState<boolean>(false)
const queryClient = useQueryClient()

// Add refs to store timer IDs
const didRefetchTimerRef = useRef<NodeJS.Timeout>()
const invalidKeyTimerRef = useRef<NodeJS.Timeout>()

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

const saveConfiguration = useCallback(async () => {
vscode.postMessage({
type: "upsertApiConfiguration",
text: "default",
apiConfiguration: apiConfiguration,
})

const waitForStateUpdate = new Promise<void>((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()
}
}
window.addEventListener("message", messageHandler)
})

try {
await waitForStateUpdate
} catch (error) {
console.error("Failed to save configuration:", error)
}
}, [apiConfiguration])

const requestModels = useCallback(async () => {
vscode.postMessage({ type: "flushRouterModels", text: "unbound" })

const modelsPromise = new Promise<void>((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)
didRefetchTimerRef.current = setTimeout(() => setDidRefetch(false), 3000)
} else {
setIsInvalidKey(true)
invalidKeyTimerRef.current = setTimeout(() => setIsInvalidKey(false), 3000)
}
}, [saveConfiguration, requestModels])

return (
<>
<VSCodeTextField
Expand All @@ -47,6 +141,24 @@ export const Unbound = ({ apiConfiguration, setApiConfigurationField, routerMode
{t("settings:providers.getUnboundApiKey")}
</VSCodeButtonLink>
)}
<div className="flex justify-end">
<Button variant="outline" onClick={handleRefresh} className="w-1/2 max-w-xs">
<div className="flex items-center gap-2 justify-center">
<span className="codicon codicon-refresh" />
{t("settings:providers.refreshModels.label")}
</div>
</Button>
</div>
{didRefetch && (
<div className="flex items-center text-vscode-charts-green">
{t("settings:providers.unboundRefreshModelsSuccess")}
</div>
)}
{isInvalidKey && (
<div className="flex items-center text-vscode-errorForeground">
{t("settings:providers.unboundInvalidApiKey")}
</div>
)}
<ModelPicker
apiConfiguration={apiConfiguration}
defaultModelId={unboundDefaultModelId}
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/ca/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Clau API d'Unbound",
"getUnboundApiKey": "Obtenir clau API d'Unbound",
"unboundRefreshModelsSuccess": "Llista de models actualitzada! Ara podeu seleccionar entre els últims models.",
"unboundInvalidApiKey": "Clau API no vàlida. Si us plau, comproveu la vostra clau API i torneu-ho a provar.",
"humanRelay": {
"description": "No es requereix clau API, però l'usuari necessita ajuda per copiar i enganxar informació al xat d'IA web.",
"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ó."
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/de/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Unbound API-Schlüssel",
"getUnboundApiKey": "Unbound API-Schlüssel erhalten",
"unboundRefreshModelsSuccess": "Modellliste aktualisiert! Sie können jetzt aus den neuesten Modellen auswählen.",
"unboundInvalidApiKey": "Ungültiger API-Schlüssel. Bitte überprüfen Sie Ihren API-Schlüssel und versuchen Sie es erneut.",
"humanRelay": {
"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.",
"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."
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Unbound API Key",
"getUnboundApiKey": "Get Unbound API Key",
"unboundRefreshModelsSuccess": "Models list updated! You can now select from the latest models.",
"unboundInvalidApiKey": "Invalid API key. Please check your API key and try again.",
"humanRelay": {
"description": "No API key is required, but the user needs to help copy and paste the information to the web chat AI.",
"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."
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/es/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Clave API de Unbound",
"getUnboundApiKey": "Obtener clave API de Unbound",
"unboundRefreshModelsSuccess": "¡Lista de modelos actualizada! Ahora puede seleccionar entre los últimos modelos.",
"unboundInvalidApiKey": "Clave API inválida. Por favor, verifique su clave API e inténtelo de nuevo.",
"humanRelay": {
"description": "No se requiere clave API, pero el usuario necesita ayudar a copiar y pegar la información en el chat web de IA.",
"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."
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/fr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Clé API Unbound",
"getUnboundApiKey": "Obtenir la clé API Unbound",
"unboundRefreshModelsSuccess": "Liste des modèles mise à jour ! Vous pouvez maintenant sélectionner parmi les derniers modèles.",
"unboundInvalidApiKey": "Clé API invalide. Veuillez vérifier votre clé API et réessayer.",
"humanRelay": {
"description": "Aucune clé API n'est requise, mais l'utilisateur doit aider à copier et coller les informations dans le chat web de l'IA.",
"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."
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/hi/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Unbound API कुंजी",
"getUnboundApiKey": "Unbound API कुंजी प्राप्त करें",
"unboundRefreshModelsSuccess": "मॉडल सूची अपडेट हो गई है! अब आप नवीनतम मॉडलों में से चुन सकते हैं।",
"unboundInvalidApiKey": "अमान्य API कुंजी। कृपया अपनी API कुंजी की जांच करें और पुनः प्रयास करें।",
"humanRelay": {
"description": "कोई API कुंजी आवश्यक नहीं है, लेकिन उपयोगकर्ता को वेब चैट AI में जानकारी कॉपी और पेस्ट करने में मदद करनी होगी।",
"instructions": "उपयोग के दौरान, एक डायलॉग बॉक्स पॉप अप होगा और वर्तमान संदेश स्वचालित रूप से क्लिपबोर्ड पर कॉपी हो जाएगा। आपको इन्हें AI के वेब संस्करणों (जैसे ChatGPT या Claude) में पेस्ट करना होगा, फिर AI की प्रतिक्रिया को डायलॉग बॉक्स में वापस कॉपी करें और पुष्टि बटन पर क्लिक करें।"
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/it/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Chiave API Unbound",
"getUnboundApiKey": "Ottieni chiave API Unbound",
"unboundRefreshModelsSuccess": "Lista dei modelli aggiornata! Ora puoi selezionare tra gli ultimi modelli.",
"unboundInvalidApiKey": "Chiave API non valida. Controlla la tua chiave API e riprova.",
"humanRelay": {
"description": "Non è richiesta alcuna chiave API, ma l'utente dovrà aiutare a copiare e incollare le informazioni nella chat web AI.",
"instructions": "Durante l'uso, apparirà una finestra di dialogo e il messaggio corrente verrà automaticamente copiato negli appunti. Dovrai incollarlo nelle versioni web dell'AI (come ChatGPT o Claude), quindi copiare la risposta dell'AI nella finestra di dialogo e fare clic sul pulsante di conferma."
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/ja/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Unbound APIキー",
"getUnboundApiKey": "Unbound APIキーを取得",
"unboundRefreshModelsSuccess": "モデルリストが更新されました!最新のモデルから選択できます。",
"unboundInvalidApiKey": "無効なAPIキーです。APIキーを確認して、もう一度お試しください。",
"humanRelay": {
"description": "APIキーは不要ですが、ユーザーはウェブチャットAIに情報をコピー&ペーストする必要があります。",
"instructions": "使用中にダイアログボックスが表示され、現在のメッセージが自動的にクリップボードにコピーされます。これらをウェブ版のAI(ChatGPTやClaudeなど)に貼り付け、AIの返答をダイアログボックスにコピーして確認ボタンをクリックする必要があります。"
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/ko/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Unbound API 키",
"getUnboundApiKey": "Unbound API 키 받기",
"unboundRefreshModelsSuccess": "모델 목록이 업데이트되었습니다! 이제 최신 모델에서 선택할 수 있습니다.",
"unboundInvalidApiKey": "잘못된 API 키입니다. API 키를 확인하고 다시 시도해 주세요.",
"humanRelay": {
"description": "API 키가 필요하지 않지만, 사용자가 웹 채팅 AI에 정보를 복사하여 붙여넣어야 합니다.",
"instructions": "사용 중에 대화 상자가 나타나고 현재 메시지가 자동으로 클립보드에 복사됩니다. 이를 웹 버전 AI(예: ChatGPT 또는 Claude)에 붙여넣은 다음, AI의 응답을 대화 상자에 복사하고 확인 버튼을 클릭해야 합니다."
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/nl/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Unbound API-sleutel",
"getUnboundApiKey": "Unbound API-sleutel ophalen",
"unboundRefreshModelsSuccess": "Modellenlijst bijgewerkt! U kunt nu kiezen uit de nieuwste modellen.",
"unboundInvalidApiKey": "Ongeldige API-sleutel. Controleer uw API-sleutel en probeer het opnieuw.",
"humanRelay": {
"description": "Geen API-sleutel vereist, maar de gebruiker moet helpen met kopiëren en plakken naar de webchat-AI.",
"instructions": "Tijdens gebruik verschijnt een dialoogvenster en wordt het huidige bericht automatisch naar het klembord gekopieerd. Je moet deze plakken in webversies van AI (zoals ChatGPT of Claude), vervolgens het antwoord van de AI terugkopiëren naar het dialoogvenster en op bevestigen klikken."
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/pl/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Klucz API Unbound",
"getUnboundApiKey": "Uzyskaj klucz API Unbound",
"unboundRefreshModelsSuccess": "Lista modeli zaktualizowana! Możesz teraz wybierać spośród najnowszych modeli.",
"unboundInvalidApiKey": "Nieprawidłowy klucz API. Sprawdź swój klucz API i spróbuj ponownie.",
"humanRelay": {
"description": "Nie jest wymagany klucz API, ale użytkownik będzie musiał pomóc w kopiowaniu i wklejaniu informacji do czatu internetowego AI.",
"instructions": "Podczas użytkowania pojawi się okno dialogowe, a bieżąca wiadomość zostanie automatycznie skopiowana do schowka. Będziesz musiał wkleić ją do internetowych wersji AI (takich jak ChatGPT lub Claude), a następnie skopiować odpowiedź AI z powrotem do okna dialogowego i kliknąć przycisk potwierdzenia."
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/pt-BR/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Chave de API Unbound",
"getUnboundApiKey": "Obter chave de API Unbound",
"unboundRefreshModelsSuccess": "Lista de modelos atualizada! Agora você pode selecionar entre os modelos mais recentes.",
"unboundInvalidApiKey": "Chave API inválida. Por favor, verifique sua chave API e tente novamente.",
"humanRelay": {
"description": "Não é necessária chave de API, mas o usuário precisa ajudar a copiar e colar as informações para a IA do chat web.",
"instructions": "Durante o uso, uma caixa de diálogo será exibida e a mensagem atual será copiada para a área de transferência automaticamente. Você precisa colar isso nas versões web de IA (como ChatGPT ou Claude), depois copiar a resposta da IA de volta para a caixa de diálogo e clicar no botão confirmar."
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/ru/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Unbound API-ключ",
"getUnboundApiKey": "Получить Unbound API-ключ",
"unboundRefreshModelsSuccess": "Список моделей обновлен! Теперь вы можете выбрать из последних моделей.",
"unboundInvalidApiKey": "Недействительный API-ключ. Пожалуйста, проверьте ваш API-ключ и попробуйте снова.",
"humanRelay": {
"description": "API-ключ не требуется, но пользователю нужно вручную копировать и вставлять информацию в веб-чат ИИ.",
"instructions": "Во время использования появится диалоговое окно, и текущее сообщение будет скопировано в буфер обмена автоматически. Вам нужно вставить его в веб-версию ИИ (например, ChatGPT или Claude), затем скопировать ответ ИИ обратно в диалоговое окно и нажать кнопку подтверждения."
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/i18n/locales/tr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@
},
"unboundApiKey": "Unbound API Anahtarı",
"getUnboundApiKey": "Unbound API Anahtarı Al",
"unboundRefreshModelsSuccess": "Model listesi güncellendi! Artık en son modeller arasından seçim yapabilirsiniz.",
"unboundInvalidApiKey": "Geçersiz API anahtarı. Lütfen API anahtarınızı kontrol edin ve tekrar deneyin.",
"humanRelay": {
"description": "API anahtarı gerekmez, ancak kullanıcının bilgileri web sohbet yapay zekasına kopyalayıp yapıştırması gerekir.",
"instructions": "Kullanım sırasında bir iletişim kutusu açılacak ve mevcut mesaj otomatik olarak panoya kopyalanacaktır. Bunları web yapay zekalarına (ChatGPT veya Claude gibi) yapıştırmanız, ardından yapay zekanın yanıtını iletişim kutusuna kopyalayıp onay düğmesine tıklamanız gerekir."
Expand Down
Loading
Loading