Merge branch 'main' into litellm-provider-config, revert modelCache logic changes

This commit is contained in:
slytechnical 2025-05-19 10:43:48 -05:00
commit cd02cd8a03
28 changed files with 279 additions and 31 deletions

View file

@ -0,0 +1,11 @@
---
"roo-cline": patch
---
New models for the Chutes provider:
- Qwen/Qwen3-235B-A22B
- Qwen/Qwen3-32B
- Qwen/Qwen3-30B-A3B
- Qwen/Qwen3-14B
- Qwen/Qwen3-8B

View file

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

4
.gitignore vendored
View file

@ -36,3 +36,7 @@ logs
# Vite development
.vite-port
# IntelliJ and Qodo plugin folders
.idea/
.qodo/

View file

@ -68,3 +68,7 @@ assets/docs/**
# Include .env file for telemetry
!.env
# Ignore IntelliJ and Qodo plugin folders
.idea/**
.qodo/**

View file

@ -34,10 +34,17 @@ const copyWasmFiles = {
// tiktoken WASM file
fs.copyFileSync(
path.join(nodeModulesDir, "tiktoken", "tiktoken_bg.wasm"),
path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
path.join(distDir, "tiktoken_bg.wasm"),
)
// Also copy to the workers directory
fs.mkdirSync(path.join(distDir, "workers"), { recursive: true })
fs.copyFileSync(
path.join(nodeModulesDir, "tiktoken", "lite", "tiktoken_bg.wasm"),
path.join(distDir, "workers", "tiktoken_bg.wasm"),
)
// Main tree-sitter WASM file
fs.copyFileSync(
path.join(nodeModulesDir, "web-tree-sitter", "tree-sitter.wasm"),

View file

@ -6,6 +6,7 @@ import NodeCache from "node-cache"
import { ContextProxy } from "../../../core/config/ContextProxy"
import { getCacheDirectoryPath } from "../../../shared/storagePathManager"
import { RouterName, ModelRecord } from "../../../shared/api"
import { fileExistsAtPath } from "../../../utils/fs"
import { getOpenRouterModels } from "./openrouter"
import { getRequestyModels } from "./requesty"
@ -21,6 +22,14 @@ async function writeModels(router: RouterName, data: ModelRecord) {
await fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data))
}
async function readModels(router: RouterName): Promise<ModelRecord | undefined> {
const filename = `${router}_models.json`
const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath)
const filePath = path.join(cacheDir, filename)
const exists = await fileExistsAtPath(filePath)
return exists ? JSON.parse(await fs.readFile(filePath, "utf8")) : undefined
}
/**
* Get models from the cache or fetch them from the provider and cache them.
* There are two caches:
@ -37,29 +46,26 @@ export const getModels = async (
apiKey: string | undefined = undefined,
baseUrl: string | undefined = undefined,
): Promise<ModelRecord> => {
// If this call is meant for a refresh (indicated by apiKey/baseUrl for specific routers),
// the memory cache should have been flushed by the caller (e.g., webviewMessageHandler).
// Otherwise, for general calls, check memory cache first.
const modelsFromMemory = memoryCache.get<ModelRecord>(router)
if (modelsFromMemory) {
return modelsFromMemory
let models = memoryCache.get<ModelRecord>(router)
if (models) {
return models
}
let fetchedModels: ModelRecord
try {
switch (router) {
case "openrouter":
fetchedModels = await getOpenRouterModels()
models = await getOpenRouterModels()
break
case "requesty":
// Assuming getRequestyModels will throw if apiKey is needed and not provided or invalid.
fetchedModels = await getRequestyModels(apiKey)
// Requesty models endpoint requires an API key for per-user custom policies
models = await getRequestyModels(apiKey)
break
case "glama":
fetchedModels = await getGlamaModels()
models = await getGlamaModels()
break
case "unbound":
fetchedModels = await getUnboundModels()
// Unbound models endpoint requires an API key to fetch application specific models
models = await getUnboundModels(apiKey)
break
case "litellm":
if (!baseUrl || !apiKey) {
@ -67,7 +73,7 @@ export const getModels = async (
// However, for robustness, if called without baseUrl for litellm, it would fail in getLiteLLMModels or here.
throw new Error("Base URL and api key are required for LiteLLM models.")
}
fetchedModels = await getLiteLLMModels(apiKey || "", baseUrl)
models = await getLiteLLMModels(apiKey || "", baseUrl)
break
default:
// Ensures router is exhaustively checked if RouterName is a strict union
@ -76,11 +82,18 @@ export const getModels = async (
}
// Cache the fetched models (even if empty, to signify a successful fetch with no models)
memoryCache.set(router, fetchedModels)
await writeModels(router, fetchedModels).catch((err) =>
memoryCache.set(router, models)
await writeModels(router, models).catch((err) =>
console.error(`[getModels] Error writing ${router} models to file cache:`, err),
)
return fetchedModels
try {
models = await readModels(router)
// console.log(`[getModels] read ${router} models from file cache`)
} catch (error) {
console.error(`[getModels] error reading ${router} models from file cache`, error)
}
return models || {}
} catch (error) {
// Log the error and re-throw it so the caller can handle it (e.g., show a UI message).
console.error(`[getModels] Failed to fetch models for ${router}:`, error)
@ -89,10 +102,6 @@ export const getModels = async (
}
}
/**
* Flush models memory cache for a specific router
* @param router - The router to flush models for.
*/
export const flushModels = async (router: RouterName) => {
memoryCache.del(router)
}

View file

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

View file

@ -350,7 +350,7 @@ export class Task extends EventEmitter<ClineEvents> {
await this.providerRef.deref()?.updateTaskHistory(historyItem)
} catch (error) {
console.error("Failed to save cline messages:", error)
console.error("Failed to save Roo messages:", error)
}
}
@ -372,7 +372,7 @@ export class Task extends EventEmitter<ClineEvents> {
// simply removes the reference to this instance, but the instance is
// still alive until this promise resolves or rejects.)
if (this.abort) {
throw new Error(`[Cline#ask] task ${this.taskId}.${this.instanceId} aborted`)
throw new Error(`[RooCode#ask] task ${this.taskId}.${this.instanceId} aborted`)
}
let askTs: number
@ -492,7 +492,7 @@ export class Task extends EventEmitter<ClineEvents> {
} = {},
): Promise<undefined> {
if (this.abort) {
throw new Error(`[Cline#say] task ${this.taskId}.${this.instanceId} aborted`)
throw new Error(`[RooCode#say] task ${this.taskId}.${this.instanceId} aborted`)
}
if (partial !== undefined) {
@ -623,7 +623,7 @@ export class Task extends EventEmitter<ClineEvents> {
} catch (error) {
this.providerRef
.deref()
?.log(`Error failed to add reply from subtast into conversation of parent task, error: ${error}`)
?.log(`Error failed to add reply from subtask into conversation of parent task, error: ${error}`)
throw error
}
@ -957,7 +957,7 @@ export class Task extends EventEmitter<ClineEvents> {
includeFileDetails: boolean = false,
): Promise<boolean> {
if (this.abort) {
throw new Error(`[Cline#recursivelyMakeClineRequests] task ${this.taskId}.${this.instanceId} aborted`)
throw new Error(`[RooCode#recursivelyMakeRooRequests] task ${this.taskId}.${this.instanceId} aborted`)
}
if (this.consecutiveMistakeCount >= this.consecutiveMistakeLimit) {
@ -1253,7 +1253,7 @@ export class Task extends EventEmitter<ClineEvents> {
// Need to call here in case the stream was aborted.
if (this.abort || this.abandoned) {
throw new Error(`[Cline#recursivelyMakeClineRequests] task ${this.taskId}.${this.instanceId} aborted`)
throw new Error(`[RooCode#recursivelyMakeRooRequests] task ${this.taskId}.${this.instanceId} aborted`)
}
this.didCompleteReadingStream = true

View file

@ -1533,6 +1533,11 @@ export type ChutesModelId =
| "deepseek-ai/DeepSeek-V3-Base"
| "deepseek-ai/DeepSeek-R1-Zero"
| "deepseek-ai/DeepSeek-V3-0324"
| "Qwen/Qwen3-235B-A22B"
| "Qwen/Qwen3-32B"
| "Qwen/Qwen3-30B-A3B"
| "Qwen/Qwen3-14B"
| "Qwen/Qwen3-8B"
| "microsoft/MAI-DS-R1-FP8"
| "tngtech/DeepSeek-R1T-Chimera"
export const chutesDefaultModelId: ChutesModelId = "deepseek-ai/DeepSeek-R1"
@ -1663,6 +1668,51 @@ export const chutesModels = {
outputPrice: 0,
description: "DeepSeek V3 (0324) model.",
},
"Qwen/Qwen3-235B-A22B": {
maxTokens: 32768,
contextWindow: 40960,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 235B A22B model.",
},
"Qwen/Qwen3-32B": {
maxTokens: 32768,
contextWindow: 40960,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 32B model.",
},
"Qwen/Qwen3-30B-A3B": {
maxTokens: 32768,
contextWindow: 40960,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 30B A3B model.",
},
"Qwen/Qwen3-14B": {
maxTokens: 32768,
contextWindow: 40960,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 14B model.",
},
"Qwen/Qwen3-8B": {
maxTokens: 32768,
contextWindow: 40960,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 8B model.",
},
"microsoft/MAI-DS-R1-FP8": {
maxTokens: 32768,
contextWindow: 163840,

View file

@ -21,5 +21,5 @@
"useUnknownInCatchVariables": false
},
"include": ["src/**/*", "scripts/**/*", ".changeset/**/*"],
"exclude": ["node_modules", ".vscode-test", "webview-ui"]
"exclude": ["node_modules", ".vscode-test", "webview-ui", ".idea", ".qodo"]
}

View file

@ -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"
@ -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>(
@ -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
@ -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}

View file

@ -188,6 +188,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ó."

View file

@ -188,6 +188,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."

View file

@ -188,6 +188,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."

View file

@ -188,6 +188,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."

View file

@ -188,6 +188,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."

View file

@ -188,6 +188,8 @@
},
"unboundApiKey": "Unbound API कुंजी",
"getUnboundApiKey": "Unbound API कुंजी प्राप्त करें",
"unboundRefreshModelsSuccess": "मॉडल सूची अपडेट हो गई है! अब आप नवीनतम मॉडलों में से चुन सकते हैं।",
"unboundInvalidApiKey": "अमान्य API कुंजी। कृपया अपनी API कुंजी की जांच करें और पुनः प्रयास करें।",
"humanRelay": {
"description": "कोई API कुंजी आवश्यक नहीं है, लेकिन उपयोगकर्ता को वेब चैट AI में जानकारी कॉपी और पेस्ट करने में मदद करनी होगी।",
"instructions": "उपयोग के दौरान, एक डायलॉग बॉक्स पॉप अप होगा और वर्तमान संदेश स्वचालित रूप से क्लिपबोर्ड पर कॉपी हो जाएगा। आपको इन्हें AI के वेब संस्करणों (जैसे ChatGPT या Claude) में पेस्ट करना होगा, फिर AI की प्रतिक्रिया को डायलॉग बॉक्स में वापस कॉपी करें और पुष्टि बटन पर क्लिक करें।"

View file

@ -188,6 +188,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."

View file

@ -188,6 +188,8 @@
},
"unboundApiKey": "Unbound APIキー",
"getUnboundApiKey": "Unbound APIキーを取得",
"unboundRefreshModelsSuccess": "モデルリストが更新されました!最新のモデルから選択できます。",
"unboundInvalidApiKey": "無効なAPIキーです。APIキーを確認して、もう一度お試しください。",
"humanRelay": {
"description": "APIキーは不要ですが、ユーザーはウェブチャットAIに情報をコピーペーストする必要があります。",
"instructions": "使用中にダイアログボックスが表示され、現在のメッセージが自動的にクリップボードにコピーされます。これらをウェブ版のAIChatGPTやClaudeなどに貼り付け、AIの返答をダイアログボックスにコピーして確認ボタンをクリックする必要があります。"

View file

@ -188,6 +188,8 @@
},
"unboundApiKey": "Unbound API 키",
"getUnboundApiKey": "Unbound API 키 받기",
"unboundRefreshModelsSuccess": "모델 목록이 업데이트되었습니다! 이제 최신 모델에서 선택할 수 있습니다.",
"unboundInvalidApiKey": "잘못된 API 키입니다. API 키를 확인하고 다시 시도해 주세요.",
"humanRelay": {
"description": "API 키가 필요하지 않지만, 사용자가 웹 채팅 AI에 정보를 복사하여 붙여넣어야 합니다.",
"instructions": "사용 중에 대화 상자가 나타나고 현재 메시지가 자동으로 클립보드에 복사됩니다. 이를 웹 버전 AI(예: ChatGPT 또는 Claude)에 붙여넣은 다음, AI의 응답을 대화 상자에 복사하고 확인 버튼을 클릭해야 합니다."

View file

@ -188,6 +188,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."

View file

@ -188,6 +188,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."

View file

@ -188,6 +188,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."

View file

@ -188,6 +188,8 @@
},
"unboundApiKey": "Unbound API-ключ",
"getUnboundApiKey": "Получить Unbound API-ключ",
"unboundRefreshModelsSuccess": "Список моделей обновлен! Теперь вы можете выбрать из последних моделей.",
"unboundInvalidApiKey": "Недействительный API-ключ. Пожалуйста, проверьте ваш API-ключ и попробуйте снова.",
"humanRelay": {
"description": "API-ключ не требуется, но пользователю нужно вручную копировать и вставлять информацию в веб-чат ИИ.",
"instructions": "Во время использования появится диалоговое окно, и текущее сообщение будет скопировано в буфер обмена автоматически. Вам нужно вставить его в веб-версию ИИ (например, ChatGPT или Claude), затем скопировать ответ ИИ обратно в диалоговое окно и нажать кнопку подтверждения."

View file

@ -188,6 +188,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."

View file

@ -188,6 +188,8 @@
},
"unboundApiKey": "Khóa API Unbound",
"getUnboundApiKey": "Lấy khóa API Unbound",
"unboundRefreshModelsSuccess": "Đã cập nhật danh sách mô hình! Bây giờ bạn có thể chọn từ các mô hình mới nhất.",
"unboundInvalidApiKey": "Khóa API không hợp lệ. Vui lòng kiểm tra khóa API của bạn và thử lại.",
"humanRelay": {
"description": "Không cần khóa API, nhưng người dùng cần giúp sao chép và dán thông tin vào AI trò chuyện web.",
"instructions": "Trong quá trình sử dụng, một hộp thoại sẽ xuất hiện và tin nhắn hiện tại sẽ được tự động sao chép vào clipboard. Bạn cần dán chúng vào các phiên bản web của AI (như ChatGPT hoặc Claude), sau đó sao chép phản hồi của AI trở lại hộp thoại và nhấp vào nút xác nhận."

View file

@ -188,6 +188,8 @@
},
"unboundApiKey": "Unbound API 密钥",
"getUnboundApiKey": "获取 Unbound API 密钥",
"unboundRefreshModelsSuccess": "模型列表已更新!您现在可以从最新模型中选择。",
"unboundInvalidApiKey": "无效的API密钥。请检查您的API密钥并重试。",
"humanRelay": {
"description": "不需要 API 密钥,但用户需要帮助将信息复制并粘贴到网页聊天 AI。",
"instructions": "使用期间,将弹出对话框并自动将当前消息复制到剪贴板。您需要将这些内容粘贴到 AI 的网页版本(如 ChatGPT 或 Claude然后将 AI 的回复复制回对话框并点击确认按钮。"

View file

@ -188,6 +188,8 @@
},
"unboundApiKey": "Unbound API 金鑰",
"getUnboundApiKey": "取得 Unbound API 金鑰",
"unboundRefreshModelsSuccess": "模型列表已更新!您現在可以從最新模型中選擇。",
"unboundInvalidApiKey": "無效的API金鑰。請檢查您的API金鑰並重試。",
"humanRelay": {
"description": "不需要 API 金鑰,但使用者需要協助將資訊複製並貼上到網頁聊天 AI。",
"instructions": "使用期間會彈出對話框,並自動將目前訊息複製到剪貼簿。您需要將這些內容貼上到網頁版 AI如 ChatGPT 或 Claude然後將 AI 的回覆複製回對話框並點選確認按鈕。"