litellm working with single provider models query

This commit is contained in:
slytechnical 2025-05-29 22:20:30 -05:00
parent 6762b579b2
commit b6a2d5827e
45 changed files with 964 additions and 736 deletions

View file

@ -4,7 +4,7 @@ import { vitest } from "vitest"
import { Anthropic } from "@anthropic-ai/sdk"
import { ApiHandlerOptions } from "../../../shared/api"
import { OpenAiHandler } from "../openai"
import { OpenAiHandler } from "../openai-compatible"
const mockCreate = vitest.fn()

View file

@ -1,7 +1,7 @@
// npx vitest run api/providers/__tests__/openai.spec.ts
import { vitest, vi } from "vitest"
import { OpenAiHandler } from "../openai"
import { OpenAiHandler } from "../openai-compatible"
import { ApiHandlerOptions } from "../../../shared/api"
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"

View file

@ -4,7 +4,7 @@ import type { ApiHandlerOptions } from "../../shared/api"
import type { ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
import { OpenAiHandler } from "./openai"
import { OpenAiHandler } from "./openai-compatible"
export class DeepSeekHandler extends OpenAiHandler {
constructor(options: ApiHandlerOptions) {

View file

@ -0,0 +1,100 @@
import { ModelRecord, GetModelsOptions, RouterName } from "../../../shared/api"
import { ProviderSettings } from "@roo-code/types"
import { WebviewMessage } from "../../../shared/WebviewMessage"
// Actual model fetching functions from individual provider files
// These will be called by the modelCache.ts:getModels function,
// so the strategies just need to return the correct GetModelsOptions object.
// The API keys are typically handled within the getModels call or the specific fetchers if needed directly.
export interface IModelProviderStrategy {
getOptions: (
apiConfiguration: ProviderSettings,
message?: WebviewMessage, // For providers like LiteLLM that might take credentials from message
) => GetModelsOptions | null
// fetchModels is not strictly needed here anymore if getModels from modelCache.ts is the single entry point
// However, if we want to keep the pattern of strategies being fully responsible for fetching,
// they would call the actual fetch functions (e.g., getOpenRouterModels, getLiteLLMModels)
// For now, let's assume the strategy's main job is to produce the correct GetModelsOptions
// and the actual fetching is centralized via modelCache.getModels(options).
}
const openRouterStrategy: IModelProviderStrategy = {
getOptions: () => ({ provider: "openrouter" }),
}
const requestyStrategy: IModelProviderStrategy = {
getOptions: (apiConfig) => ({ provider: "requesty", apiKey: apiConfig.requestyApiKey }),
}
const glamaStrategy: IModelProviderStrategy = {
getOptions: () => ({ provider: "glama" }),
}
const unboundStrategy: IModelProviderStrategy = {
getOptions: (apiConfig) => ({ provider: "unbound", apiKey: apiConfig.unboundApiKey }),
}
const litellmStrategy: IModelProviderStrategy = {
getOptions: (apiConfig, message) => {
const apiKey = message?.values?.litellmApiKey || apiConfig.litellmApiKey
const baseUrl = message?.values?.litellmBaseUrl || apiConfig.litellmBaseUrl
if (!apiKey || !baseUrl) {
// Error will be handled by the caller in webviewMessageHandler
return null
}
return { provider: "litellm", apiKey, baseUrl }
},
}
const ollamaStrategy: IModelProviderStrategy = {
getOptions: (apiConfig, message) => {
const baseUrl = message?.values?.baseUrl || apiConfig.ollamaBaseUrl
return { provider: "ollama", baseUrl: baseUrl || undefined }
},
}
const lmStudioStrategy: IModelProviderStrategy = {
getOptions: (apiConfig, message) => {
const baseUrl = message?.values?.baseUrl || apiConfig.lmStudioBaseUrl
return { provider: "lmstudio", baseUrl: baseUrl || undefined }
},
}
const vsCodeLmStrategy: IModelProviderStrategy = {
getOptions: () => {
return { provider: "vscodelm" }
},
}
const openAICompatibleStrategy: IModelProviderStrategy = {
getOptions: (apiConfig, message) => {
const baseUrl = message?.values?.baseUrl || apiConfig.openAiBaseUrl
if (!baseUrl) {
// webviewMessageHandler will catch this null and send an error if baseUrl is essential
// For this strategy, we consider baseUrl essential for forming the options.
console.warn("[OpenAICompatibleStrategy] Base URL is missing.")
return null
}
return {
provider: "openai-compatible",
baseUrl,
apiKey: message?.values?.apiKey || apiConfig.openAiApiKey,
headers: message?.values?.openAiHeaders || apiConfig.openAiHeaders,
// Azure-specific flags can be part of apiConfig and implicitly used by OpenAiHandler if needed,
// or explicitly passed if the GetModelsOptions for openai-compatible is extended.
}
},
}
export const modelProviderStrategies: Record<RouterName, IModelProviderStrategy | undefined> = {
openrouter: openRouterStrategy,
requesty: requestyStrategy,
glama: glamaStrategy,
unbound: unboundStrategy,
litellm: litellmStrategy,
ollama: ollamaStrategy,
lmstudio: lmStudioStrategy,
vscodelm: vsCodeLmStrategy,
"openai-compatible": openAICompatibleStrategy,
}

View file

@ -68,6 +68,9 @@ export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise
return models
} catch (error: any) {
console.error("Error fetching LiteLLM models:", error.message ? error.message : error)
console.log(
`[DEBUG] LiteLLM error details - isAxiosError: ${axios.isAxiosError(error)}, has response: ${!!(error as any)?.response}, has request: ${!!(error as any)?.request}`,
)
if (axios.isAxiosError(error) && error.response) {
throw new Error(
`Failed to fetch LiteLLM models: ${error.response.status} ${error.response.statusText}. Check base URL and API key.`,

View file

@ -5,7 +5,7 @@ import NodeCache from "node-cache"
import { ContextProxy } from "../../../core/config/ContextProxy"
import { getCacheDirectoryPath } from "../../../utils/storage"
import { RouterName, ModelRecord } from "../../../shared/api"
import { RouterName, ModelRecord, GetModelsOptions } from "../../../shared/api"
import { fileExistsAtPath } from "../../../utils/fs"
import { getOpenRouterModels } from "./openrouter"
@ -13,21 +13,42 @@ import { getRequestyModels } from "./requesty"
import { getGlamaModels } from "./glama"
import { getUnboundModels } from "./unbound"
import { getLiteLLMModels } from "./litellm"
import { GetModelsOptions } from "../../../shared/api"
import { getOllamaModels } from "../ollama"
import { getLmStudioModels } from "../lmstudio"
import { getVsCodeLmModels } from "../vscode-lm"
import { getOpenAiCompatibleModels } from "../openai-compatible"
const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
async function writeModels(router: RouterName, data: ModelRecord) {
const filename = `${router}_models.json`
const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath)
await fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data))
try {
await fs.writeFile(path.join(cacheDir, filename), JSON.stringify(data))
} catch (writeError) {
console.error(`[writeModels] Error writing ${router} models to file cache:`, writeError)
// Optionally, re-throw or handle as per application's error strategy
}
}
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
try {
const exists = await fileExistsAtPath(filePath)
if (exists) {
const fileContent = await fs.readFile(filePath, "utf8")
const data = JSON.parse(fileContent) as ModelRecord
console.log(`[readModels] Successfully read and parsed ${filePath}. Data: ${JSON.stringify(data)}`)
return data
}
console.log(`[readModels] File ${filePath} does not exist.`)
return undefined
} catch (readError) {
console.error(`[readModels] Error reading ${router} models from file cache at ${filePath}:`, readError)
return undefined
}
}
/**
@ -44,55 +65,82 @@ async function readModels(router: RouterName): Promise<ModelRecord | undefined>
export const getModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
const { provider } = options
let models = memoryCache.get<ModelRecord>(provider)
if (models) {
if (models && Object.keys(models).length > 0) {
console.log(`[getModels] Returning non-empty models from memory cache for ${provider}`)
return models
} else if (models) {
console.log(`[getModels] Memory cache for ${provider} is empty object, treating as miss.`)
}
models = await readModels(provider)
if (models && Object.keys(models).length > 0) {
console.log(
`[getModels] Returning non-empty models from file cache for ${provider} and populating memory cache.`,
)
memoryCache.set(provider, models) // Populate memory cache with non-empty file cache data
return models
} else if (models) {
console.log(`[getModels] File cache for ${provider} is empty object, treating as miss.`)
}
console.log(`[getModels] No valid cache hit for ${provider}, attempting to fetch from provider.`)
try {
let fetchedModels: ModelRecord | undefined
switch (provider) {
case "openrouter":
models = await getOpenRouterModels()
fetchedModels = await getOpenRouterModels()
break
case "requesty":
// Requesty models endpoint requires an API key for per-user custom policies
models = await getRequestyModels(options.apiKey)
fetchedModels = await getRequestyModels(options.apiKey)
break
case "glama":
models = await getGlamaModels()
fetchedModels = await getGlamaModels()
break
case "unbound":
// Unbound models endpoint requires an API key to fetch application specific models
models = await getUnboundModels(options.apiKey)
fetchedModels = await getUnboundModels(options.apiKey)
break
case "litellm":
// Type safety ensures apiKey and baseUrl are always provided for litellm
models = await getLiteLLMModels(options.apiKey, options.baseUrl)
if (!options.apiKey || !options.baseUrl) {
throw new Error("LiteLLM provider requires apiKey and baseUrl.")
}
fetchedModels = await getLiteLLMModels(options.apiKey, options.baseUrl)
break
case "ollama":
fetchedModels = await getOllamaModels(options.baseUrl)
break
case "lmstudio":
fetchedModels = await getLmStudioModels(options.baseUrl)
break
case "vscodelm":
fetchedModels = await getVsCodeLmModels()
break
case "openai-compatible": {
const opts = options as Extract<GetModelsOptions, { provider: "openai-compatible" }>
if (!opts.baseUrl) {
throw new Error("OpenAI-Compatible provider requires baseUrl.")
}
fetchedModels = await getOpenAiCompatibleModels(opts.baseUrl, opts.apiKey, opts.headers)
break
}
default: {
// Ensures router is exhaustively checked if RouterName is a strict union
const exhaustiveCheck: never = provider
throw new Error(`Unknown provider: ${exhaustiveCheck}`)
}
}
// Cache the fetched models (even if empty, to signify a successful fetch with no models)
memoryCache.set(provider, models)
await writeModels(provider, models).catch((err) =>
console.error(`[getModels] Error writing ${provider} models to file cache:`, err),
)
try {
models = await readModels(provider)
// console.log(`[getModels] read ${router} models from file cache`)
} catch (error) {
console.error(`[getModels] error reading ${provider} models from file cache`, error)
}
return models || {}
// Ensure fetchedModels is not undefined before caching. If a fetch truly returns no models, it should be an empty object.
const modelsToCache = fetchedModels || {}
console.log(`[getModels] Successfully fetched models for ${provider}. Caching now.`)
memoryCache.set(provider, modelsToCache)
await writeModels(provider, modelsToCache)
return modelsToCache
} 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 in modelCache for ${provider}:`, error)
throw error // Re-throw the original error to be handled by the caller.
console.error(`[getModels] Failed to fetch models for ${provider}:`, error)
console.log(`[getModels] Clearing cache for ${provider} due to fetch error.`)
memoryCache.set(provider, {}) // Clear memory cache by setting to empty object
await writeModels(provider, {}) // Clear persisted file cache by writing empty object
throw error // Re-throw the original error
}
}
@ -101,5 +149,7 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
* @param router - The router to flush models for.
*/
export const flushModels = async (router: RouterName) => {
memoryCache.del(router)
console.log(`[flushModels] Flushing both memory and file cache for ${router}`)
memoryCache.del(router) // Deleting from memory cache is fine, will be treated as miss
await writeModels(router, {}) // Write an empty object to clear the file cache
}

View file

@ -4,7 +4,7 @@ export { AwsBedrockHandler } from "./bedrock"
export { OpenRouterHandler } from "./openrouter"
export { VertexHandler } from "./vertex"
export { AnthropicVertexHandler } from "./anthropic-vertex"
export { OpenAiHandler } from "./openai"
export { OpenAiHandler } from "./openai-compatible"
export { OllamaHandler } from "./ollama"
export { LmStudioHandler } from "./lmstudio"
export { GeminiHandler } from "./gemini"

View file

@ -4,7 +4,7 @@ import axios from "axios"
import type { ModelInfo } from "@roo-code/types"
import { ApiHandlerOptions, openAiModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandlerOptions, openAiModelInfoSaneDefaults, ModelRecord } from "../../shared/api"
import { XmlMatcher } from "../../utils/xml-matcher"
import { convertToOpenAiMessages } from "../transform/openai-format"
@ -163,16 +163,31 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
}
}
export async function getLmStudioModels(baseUrl = "http://localhost:1234") {
export async function getLmStudioModels(baseUrl: string = "http://localhost:1234"): Promise<ModelRecord> {
try {
if (!URL.canParse(baseUrl)) {
return []
if (baseUrl && !URL.canParse(baseUrl)) {
console.warn(
`Invalid LMStudio baseUrl provided: ${baseUrl}. Using default or expecting empty if intentionally omitted.`,
)
if (baseUrl !== "http://localhost:1234") return {}
}
const targetUrl = baseUrl || "http://localhost:1234"
const response = await axios.get(`${baseUrl}/v1/models`)
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
return [...new Set<string>(modelsArray)]
const response = await axios.get(`${targetUrl}/v1/models`)
const modelsArray = response.data?.data?.map((model: any) => model.id) || [] // LM Studio API returns models in data.data array with id property
const modelRecord: ModelRecord = {}
for (const modelId of new Set<string>(modelsArray)) {
modelRecord[modelId] = {
...openAiModelInfoSaneDefaults,
description: `LM Studio model: ${modelId}`,
inputPrice: undefined,
outputPrice: undefined,
}
}
return modelRecord
} catch (error) {
return []
console.error("Error fetching LM Studio models:", error)
return {}
}
}

View file

@ -4,7 +4,7 @@ import axios from "axios"
import type { ModelInfo } from "@roo-code/types"
import { ApiHandlerOptions, openAiModelInfoSaneDefaults } from "../../shared/api"
import { ApiHandlerOptions, openAiModelInfoSaneDefaults, ModelRecord } from "../../shared/api"
import { XmlMatcher } from "../../utils/xml-matcher"
import { convertToOpenAiMessages } from "../transform/openai-format"
@ -113,16 +113,31 @@ export class OllamaHandler extends BaseProvider implements SingleCompletionHandl
}
}
export async function getOllamaModels(baseUrl = "http://localhost:11434") {
export async function getOllamaModels(baseUrl: string = "http://localhost:11434"): Promise<ModelRecord> {
try {
if (!URL.canParse(baseUrl)) {
return []
if (baseUrl && !URL.canParse(baseUrl)) {
console.warn(
`Invalid Ollama baseUrl provided: ${baseUrl}. Using default or expecting empty if intentionally omitted.`,
)
if (baseUrl !== "http://localhost:11434") return {}
}
const targetUrl = baseUrl || "http://localhost:11434"
const response = await axios.get(`${baseUrl}/api/tags`)
const response = await axios.get(`${targetUrl}/api/tags`)
const modelsArray = response.data?.models?.map((model: any) => model.name) || []
return [...new Set<string>(modelsArray)]
const modelRecord: ModelRecord = {}
for (const modelId of new Set<string>(modelsArray)) {
modelRecord[modelId] = {
...openAiModelInfoSaneDefaults,
description: `Ollama model: ${modelId}`,
inputPrice: undefined,
outputPrice: undefined,
}
}
return modelRecord
} catch (error) {
return []
console.error("Error fetching Ollama models:", error)
return {}
}
}

View file

@ -4,7 +4,12 @@ import axios from "axios"
import type { ModelInfo } from "@roo-code/types"
import { ApiHandlerOptions, azureOpenAiDefaultApiVersion, openAiModelInfoSaneDefaults } from "../../shared/api"
import {
ApiHandlerOptions,
azureOpenAiDefaultApiVersion,
openAiModelInfoSaneDefaults,
ModelRecord,
} from "../../shared/api"
import { XmlMatcher } from "../../utils/xml-matcher"
@ -367,14 +372,20 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
}
}
export async function getOpenAiModels(baseUrl?: string, apiKey?: string, openAiHeaders?: Record<string, string>) {
export async function getOpenAiCompatibleModels(
baseUrl?: string,
apiKey?: string,
openAiHeaders?: Record<string, string>,
): Promise<ModelRecord> {
try {
if (!baseUrl) {
return []
console.warn("[getOpenAiModels] Base URL is missing, returning empty models.")
return {}
}
if (!URL.canParse(baseUrl)) {
return []
console.warn(`[getOpenAiModels] Invalid Base URL: ${baseUrl}, returning empty models.`)
return {}
}
const config: Record<string, any> = {}
@ -392,9 +403,21 @@ export async function getOpenAiModels(baseUrl?: string, apiKey?: string, openAiH
}
const response = await axios.get(`${baseUrl}/models`, config)
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
return [...new Set<string>(modelsArray)]
const modelsList = response.data?.data || response.data || []
const modelsArray = modelsList.map((model: any) => model.id).filter(Boolean) || []
const modelRecord: ModelRecord = {}
for (const modelId of new Set<string>(modelsArray)) {
modelRecord[modelId] = {
...openAiModelInfoSaneDefaults,
description: `OpenAI-compatible: ${modelId} (${baseUrl})`,
inputPrice: undefined,
outputPrice: undefined,
}
}
return modelRecord
} catch (error) {
return []
console.error(`[getOpenAiModels] Error fetching OpenAI-compatible models from ${baseUrl}:`, error)
return {}
}
}

View file

@ -2,9 +2,9 @@ import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import type { ModelInfo } from "@roo-code/types"
import { ApiHandlerOptions, ModelRecord, openAiModelInfoSaneDefaults } from "../../shared/api"
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
import { ApiHandlerOptions, openAiModelInfoSaneDefaults } from "../../shared/api"
import { XmlMatcher } from "../../utils/xml-matcher"
import { ApiStream } from "../transform/stream"
import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
@ -567,14 +567,32 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan
// Static blacklist of VS Code Language Model IDs that should be excluded from the model list e.g. because they will never work
const VSCODE_LM_STATIC_BLACKLIST: string[] = ["claude-3.7-sonnet", "claude-3.7-sonnet-thought"]
export async function getVsCodeLmModels() {
export async function getVsCodeLmModels(): Promise<ModelRecord> {
try {
const models = (await vscode.lm.selectChatModels({})) || []
return models.filter((model) => !VSCODE_LM_STATIC_BLACKLIST.includes(model.id))
const availableModelsMeta = (await vscode.lm.selectChatModels({})) || []
const filteredModelsMeta = availableModelsMeta.filter((model) => !VSCODE_LM_STATIC_BLACKLIST.includes(model.id))
const modelRecord: ModelRecord = {}
for (const modelMeta of filteredModelsMeta) {
// Construct a unique ID if modelMeta.id is not sufficiently unique or suitable as a key
const modelKey =
modelMeta.id || `${modelMeta.vendor}-${modelMeta.family}-${modelMeta.version}`.toLowerCase()
modelRecord[modelKey] = {
...openAiModelInfoSaneDefaults,
// id: modelMeta.id, // ID is the key in ModelRecord
description: `VSCode LM: ${modelMeta.name || modelKey} (Vendor: ${modelMeta.vendor}, Family: ${modelMeta.family})`,
contextWindow: modelMeta.maxInputTokens,
supportsImages: false, // Default for VS Code LM, can be refined if API provides this
inputPrice: undefined,
outputPrice: undefined,
// Add other relevant properties from modelMeta if they map to ModelInfo
}
}
return modelRecord
} catch (error) {
console.error(
`Error fetching VS Code LM models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
)
return []
return {} // Return empty ModelRecord on error
}
}

View file

@ -27,18 +27,14 @@ import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
import { singleCompletionHandler } from "../../utils/single-completion-handler"
import { searchCommits } from "../../utils/git"
import { exportSettings, importSettings } from "../config/importExport"
import { getOpenAiModels } from "../../api/providers/openai"
import { getOllamaModels } from "../../api/providers/ollama"
import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
import { getLmStudioModels } from "../../api/providers/lmstudio"
import { openMention } from "../mentions"
import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { getWorkspacePath } from "../../utils/path"
import { Mode, defaultModeSlug } from "../../shared/modes"
import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
import { GetModelsOptions } from "../../shared/api"
import { generateSystemPrompt } from "./generateSystemPrompt"
import { getCommand } from "../../utils/commands"
import { modelProviderStrategies } from "../../api/providers/fetchers"
const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
@ -296,104 +292,86 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
break
case "requestRouterModels":
const { apiConfiguration } = await provider.getState()
console.log("apiconfig1212", apiConfiguration, message.values)
const providerNameValue = message.values?.provider as string | undefined
const routerName = toRouterName(providerNameValue)
const flushCacheFirst = !!message.values?.flushCacheFirst
const routerModels: Partial<Record<RouterName, ModelRecord>> = {
openrouter: {},
requesty: {},
glama: {},
unbound: {},
litellm: {},
}
const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
try {
return await getModels(options)
} catch (error) {
console.error(
`Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}:`,
error,
)
throw error // Re-throw to be caught by Promise.allSettled
}
}
const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [
{ key: "openrouter", options: { provider: "openrouter" } },
{ key: "requesty", options: { provider: "requesty", apiKey: apiConfiguration.requestyApiKey } },
{ key: "glama", options: { provider: "glama" } },
{ key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
]
const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey
const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl
if (litellmApiKey && litellmBaseUrl) {
modelFetchPromises.push({
key: "litellm",
options: { provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl },
})
}
const results = await Promise.allSettled(
modelFetchPromises.map(async ({ key, options }) => {
const models = await safeGetModels(options)
return { key, models } // key is RouterName here
}),
console.log(
`[requestRouterModels] Received request for ${routerName}. flushCacheFirst: ${flushCacheFirst}. Message values:`,
message.values,
)
const fetchedRouterModels: Partial<Record<RouterName, ModelRecord>> = { ...routerModels }
if (!providerNameValue || !routerName) {
provider.postMessageToWebview({
type: "singleRouterModelFetchResponse",
success: false,
error: "Invalid or missing provider name for requestRouterModels",
values: { provider: providerNameValue || "unknown" },
})
break
}
results.forEach((result, index) => {
const routerName = modelFetchPromises[index].key // Get RouterName using index
const strategy = modelProviderStrategies[routerName]
if (result.status === "fulfilled") {
fetchedRouterModels[routerName] = result.value.models
if (!strategy || !strategy.getOptions) {
provider.postMessageToWebview({
type: "singleRouterModelFetchResponse",
success: false,
error: `Unsupported provider or strategy misconfiguration: ${routerName}`,
values: { provider: routerName },
})
break
}
const modelOptions = strategy.getOptions(apiConfiguration, message)
console.log(`[requestRouterModels] strategy.getOptions returned:`, modelOptions)
if (!modelOptions) {
provider.postMessageToWebview({
type: "singleRouterModelFetchResponse",
success: false,
error: `Required options missing for ${routerName} (e.g., API key/URL for LiteLLM/OpenAI-Compatible, or valid BaseURL for Ollama/LMStudio if passed via message.values)`,
values: { provider: routerName },
})
break
}
try {
console.log(
`[requestRouterModels] In try block. routerName: ${routerName}, flushCacheFirst: ${flushCacheFirst}`,
)
if (flushCacheFirst) {
console.log("[requestRouterModels] Condition for flushCacheFirst is TRUE. Calling flushModels.")
await flushModels(routerName)
} else {
// Handle rejection: Post a specific error message for this provider
const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)
console.error(`Error fetching models for ${routerName}:`, result.reason)
fetchedRouterModels[routerName] = {} // Ensure it's an empty object in the main routerModels message
console.log("[requestRouterModels] Condition for flushCacheFirst is FALSE. Skipping flushModels.")
}
console.log("[requestRouterModels] About to call getModels with options:", modelOptions)
const models = await getModels(modelOptions)
console.log("models1212", models)
provider.postMessageToWebview({
type: "singleRouterModelFetchResponse",
success: true,
values: { provider: routerName, models },
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(`Error fetching models for ${routerName} via requestRouterModels:`, error)
console.log(`[DEBUG] About to post error message for ${routerName}:`, errorMessage)
try {
provider.postMessageToWebview({
type: "singleRouterModelFetchResponse",
success: false,
error: errorMessage,
values: { provider: routerName },
})
console.log(`[DEBUG] Error message posted successfully for ${routerName}`)
} catch (postError) {
console.error(`[DEBUG] Failed to post error message to webview:`, postError)
}
})
provider.postMessageToWebview({
type: "routerModels",
routerModels: fetchedRouterModels as Record<RouterName, ModelRecord>,
})
break
case "requestOpenAiModels":
if (message?.values?.baseUrl && message?.values?.apiKey) {
const openAiModels = await getOpenAiModels(
message?.values?.baseUrl,
message?.values?.apiKey,
message?.values?.openAiHeaders,
)
provider.postMessageToWebview({ type: "openAiModels", openAiModels })
}
break
case "requestOllamaModels":
const ollamaModels = await getOllamaModels(message.text)
// TODO: Cache like we do for OpenRouter, etc?
provider.postMessageToWebview({ type: "ollamaModels", ollamaModels })
break
case "requestLmStudioModels":
const lmStudioModels = await getLmStudioModels(message.text)
// TODO: Cache like we do for OpenRouter, etc?
provider.postMessageToWebview({ type: "lmStudioModels", lmStudioModels })
break
case "requestVsCodeLmModels":
const vsCodeLmModels = await getVsCodeLmModels()
// TODO: Cache like we do for OpenRouter, etc?
provider.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
break
case "openImage":
openImage(message.text!)

View file

@ -15,7 +15,6 @@ import { GitCommit } from "../utils/git"
import { McpServer } from "./mcp"
import { Mode } from "./modes"
import { RouterModels } from "./api"
export interface LanguageModelChatSelector {
vendor?: string
@ -40,7 +39,6 @@ export interface ExtensionMessage {
| "enhancedPrompt"
| "commitSearchResults"
| "listApiConfig"
| "routerModels"
| "openAiModels"
| "ollamaModels"
| "lmStudioModels"
@ -93,7 +91,6 @@ export interface ExtensionMessage {
path?: string
}>
partialMessage?: ClineMessage
routerModels?: RouterModels
openAiModels?: string[]
ollamaModels?: string[]
lmStudioModels?: string[]

View file

@ -1972,7 +1972,17 @@ export const OPEN_ROUTER_REQUIRED_REASONING_BUDGET_MODELS = new Set([
"google/gemini-2.5-flash-preview-05-20:thinking",
])
const routerNames = ["openrouter", "requesty", "glama", "unbound", "litellm"] as const
const routerNames = [
"openrouter",
"requesty",
"glama",
"unbound",
"litellm",
"ollama",
"lmstudio",
"vscodelm",
"openai-compatible",
] as const
export type RouterName = (typeof routerNames)[number]
@ -1988,8 +1998,6 @@ export function toRouterName(value?: string): RouterName {
export type ModelRecord = Record<string, ModelInfo>
export type RouterModels = Record<RouterName, ModelRecord>
export const shouldUseReasoningBudget = ({
model,
settings,
@ -2045,3 +2053,13 @@ export type GetModelsOptions =
| { provider: "requesty"; apiKey?: string }
| { provider: "unbound"; apiKey?: string }
| { provider: "litellm"; apiKey: string; baseUrl: string }
| { provider: "ollama"; baseUrl?: string } // Ollama might take an optional base URL
| { provider: "lmstudio"; baseUrl?: string } // LM Studio might take an optional base URL
| { provider: "vscodelm" } // VSCodeLM likely takes no specific options here
| {
provider: "openai-compatible"
baseUrl: string
apiKey?: string
headers?: Record<string, string>
// We might not need openAiUseAzure and azureApiVersion here if getOpenAiModels can infer from baseUrl or they are passed to OpenAiHandler via general apiConfig
}

View file

@ -16,7 +16,6 @@ import {
import { vscode } from "@src/utils/vscode"
import { validateApiConfiguration } from "@src/utils/validate"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { filterProviders, filterModels } from "./utils/organizationFilters"
@ -75,6 +74,13 @@ const ApiOptions = ({
const { t } = useAppTranslation()
const { organizationAllowList } = useExtensionState()
const refetchRouterModels = useCallback(() => {
vscode.postMessage({
type: "flushRouterModels",
values: { provider: apiConfiguration.apiProvider },
})
}, [apiConfiguration.apiProvider])
const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
const headers = apiConfiguration?.openAiHeaders || {}
return Object.entries(headers)
@ -82,22 +88,15 @@ const ApiOptions = ({
useEffect(() => {
const propHeaders = apiConfiguration?.openAiHeaders || {}
if (JSON.stringify(customHeaders) !== JSON.stringify(Object.entries(propHeaders))) {
setCustomHeaders(Object.entries(propHeaders))
}
}, [apiConfiguration?.openAiHeaders, customHeaders])
// Helper to convert array of tuples to object (filtering out empty keys).
// Debounced effect to update the main configuration when local
// customHeaders state stabilizes.
useDebounce(
() => {
const currentConfigHeaders = apiConfiguration?.openAiHeaders || {}
const newHeadersObject = convertHeadersToObject(customHeaders)
// Only update if the processed object is different from the current config.
if (JSON.stringify(currentConfigHeaders) !== JSON.stringify(newHeadersObject)) {
setApiConfigurationField("openAiHeaders", newHeadersObject)
}
@ -125,61 +124,17 @@ const ApiOptions = ({
info: selectedModelInfo,
} = useSelectedModel(apiConfiguration)
const { data: routerModels, refetch: refetchRouterModels } = useRouterModels()
// Update `apiModelId` whenever `selectedModelId` changes.
useEffect(() => {
if (selectedModelId) {
setApiConfigurationField("apiModelId", selectedModelId)
}
}, [selectedModelId, setApiConfigurationField])
// Debounced refresh model updates, only executed 250ms after the user
// stops typing.
useDebounce(
() => {
if (selectedProvider === "openai") {
// Use our custom headers state to build the headers object.
const headerObject = convertHeadersToObject(customHeaders)
vscode.postMessage({
type: "requestOpenAiModels",
values: {
baseUrl: apiConfiguration?.openAiBaseUrl,
apiKey: apiConfiguration?.openAiApiKey,
customHeaders: {}, // Reserved for any additional headers
openAiHeaders: headerObject,
},
})
} else if (selectedProvider === "ollama") {
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
} else if (selectedProvider === "lmstudio") {
vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl })
} else if (selectedProvider === "vscode-lm") {
vscode.postMessage({ type: "requestVsCodeLmModels" })
} else if (selectedProvider === "litellm") {
vscode.postMessage({ type: "requestRouterModels" })
}
},
250,
[
selectedProvider,
apiConfiguration?.requestyApiKey,
apiConfiguration?.openAiBaseUrl,
apiConfiguration?.openAiApiKey,
apiConfiguration?.ollamaBaseUrl,
apiConfiguration?.lmStudioBaseUrl,
apiConfiguration?.litellmBaseUrl,
apiConfiguration?.litellmApiKey,
customHeaders,
],
)
useEffect(() => {
const apiValidationResult = validateApiConfiguration(apiConfiguration, routerModels, organizationAllowList)
const apiValidationResult = validateApiConfiguration(apiConfiguration, organizationAllowList)
setErrorMessage(apiValidationResult)
}, [apiConfiguration, routerModels, organizationAllowList, setErrorMessage])
}, [apiConfiguration, organizationAllowList, setErrorMessage])
const selectedProviderModels = useMemo(() => {
const models = MODELS_BY_PROVIDER[selectedProvider]
@ -197,13 +152,6 @@ const ApiOptions = ({
const onProviderChange = useCallback(
(value: ProviderName) => {
// It would be much easier to have a single attribute that stores
// the modelId, but we have a separate attribute for each of
// OpenRouter, Glama, Unbound, and Requesty.
// If you switch to one of these providers and the corresponding
// modelId is not set then you immediately end up in an error state.
// To address that we set the modelId to the default value for th
// provider if it's not already set.
switch (value) {
case "openrouter":
if (!apiConfiguration.openRouterModelId) {
@ -231,7 +179,6 @@ const ApiOptions = ({
}
break
}
setApiConfigurationField("apiProvider", value)
},
[
@ -247,22 +194,10 @@ const ApiOptions = ({
const docs = useMemo(() => {
const provider = PROVIDERS.find(({ value }) => value === selectedProvider)
const name = provider?.label
if (!name) {
return undefined
}
// Get the URL slug - use custom mapping if available, otherwise use the provider key.
const slugs: Record<string, string> = {
"openai-native": "openai",
openai: "openai-compatible",
}
if (!name) return undefined
const slugs: Record<string, string> = { "openai-native": "openai", openai: "openai-compatible" }
const slug = slugs[selectedProvider] || selectedProvider
return {
url: buildDocLink(`providers/${slug}`, "provider_docs"),
name,
}
return { url: buildDocLink(`providers/${slug}`, "provider_docs"), name }
}, [selectedProvider])
return (
@ -298,7 +233,6 @@ const ApiOptions = ({
<OpenRouter
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
selectedModelId={selectedModelId}
uriScheme={uriScheme}
fromWelcomeView={fromWelcomeView}
@ -310,7 +244,6 @@ const ApiOptions = ({
<Requesty
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
refetchRouterModels={refetchRouterModels}
organizationAllowList={organizationAllowList}
/>
@ -320,7 +253,6 @@ const ApiOptions = ({
<Glama
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
uriScheme={uriScheme}
organizationAllowList={organizationAllowList}
/>
@ -330,7 +262,6 @@ const ApiOptions = ({
<Unbound
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
organizationAllowList={organizationAllowList}
/>
)}
@ -427,7 +358,6 @@ const ApiOptions = ({
onValueChange={(value) => {
setApiConfigurationField("apiModelId", value)
// Clear custom ARN if not using custom ARN option.
if (value !== "custom-arn" && selectedProvider === "bedrock") {
setApiConfigurationField("awsCustomArn", "")
}

View file

@ -54,30 +54,55 @@ export const ModelPicker = ({
const [open, setOpen] = useState(false)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
const isInitialized = useRef(false)
const searchInputRef = useRef<HTMLInputElement>(null)
const modelIds = useMemo(() => {
const filteredModels = filterModels(models, apiConfiguration.apiProvider, organizationAllowList)
const currentConfiguredModelId = apiConfiguration[modelIdKey]
const modelIdsForDropdown = useMemo(() => {
const filteredModels = filterModels(models, apiConfiguration.apiProvider, organizationAllowList)
return Object.keys(filteredModels ?? {}).sort((a, b) => a.localeCompare(b))
}, [models, apiConfiguration.apiProvider, organizationAllowList])
const { id: selectedModelId, info: selectedModelInfo } = useSelectedModel(apiConfiguration)
const { id: selectedModelIdForInfo, info: selectedModelInfo } = useSelectedModel(apiConfiguration)
const [searchValue, setSearchValue] = useState(selectedModelId || "")
const [searchValue, setSearchValue] = useState(currentConfiguredModelId || "")
useEffect(() => {
const currentIdInSettings = apiConfiguration[modelIdKey]
if (!models || Object.keys(models).length === 0) {
if (currentIdInSettings !== undefined) {
setApiConfigurationField(modelIdKey, undefined)
}
if (searchValue !== "") setSearchValue("")
} else {
const availableIds = Object.keys(models)
let newIdToSet: string | undefined = undefined
if (currentIdInSettings && availableIds.includes(currentIdInSettings)) {
newIdToSet = currentIdInSettings
} else if (availableIds.includes(defaultModelId)) {
newIdToSet = defaultModelId
} else if (availableIds.length > 0) {
newIdToSet = availableIds[0]
}
if (currentIdInSettings !== newIdToSet) {
setApiConfigurationField(modelIdKey, newIdToSet)
}
const targetSearchValue = newIdToSet || ""
if (searchValue !== targetSearchValue) setSearchValue(targetSearchValue)
}
}, [models, apiConfiguration, searchValue, defaultModelId, modelIdKey, setApiConfigurationField])
const onSelect = useCallback(
(modelId: string) => {
if (!modelId) {
return
}
setOpen(false)
setApiConfigurationField(modelIdKey, modelId)
// Delay to ensure the popover is closed before setting the search value.
setTimeout(() => setSearchValue(modelId), 100)
setSearchValue(modelId)
},
[modelIdKey, setApiConfigurationField],
)
@ -85,14 +110,11 @@ export const ModelPicker = ({
const onOpenChange = useCallback(
(open: boolean) => {
setOpen(open)
// Abandon the current search if the popover is closed.
if (!open) {
// Delay to ensure the popover is closed before setting the search value.
setTimeout(() => setSearchValue(selectedModelId), 100)
setSearchValue(apiConfiguration[modelIdKey] || "")
}
},
[selectedModelId],
[apiConfiguration, modelIdKey],
)
const onClearSearch = useCallback(() => {
@ -100,15 +122,6 @@ export const ModelPicker = ({
searchInputRef.current?.focus()
}, [])
useEffect(() => {
if (!selectedModelId && !isInitialized.current) {
const initialValue = modelIds.includes(selectedModelId) ? selectedModelId : defaultModelId
setApiConfigurationField(modelIdKey, initialValue)
}
isInitialized.current = true
}, [modelIds, setApiConfigurationField, modelIdKey, selectedModelId, defaultModelId])
return (
<>
<div>
@ -120,7 +133,7 @@ export const ModelPicker = ({
role="combobox"
aria-expanded={open}
className="w-full justify-between">
<div>{selectedModelId ?? t("settings:common.select")}</div>
<div>{currentConfiguredModelId ?? t("settings:common.select")}</div>
<ChevronsUpDown className="opacity-50" />
</Button>
</PopoverTrigger>
@ -153,20 +166,20 @@ export const ModelPicker = ({
)}
</CommandEmpty>
<CommandGroup>
{modelIds.map((model) => (
{modelIdsForDropdown.map((model) => (
<CommandItem key={model} value={model} onSelect={onSelect}>
{model}
<Check
className={cn(
"size-4 p-0.5 ml-auto",
model === selectedModelId ? "opacity-100" : "opacity-0",
model === currentConfiguredModelId ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
{searchValue && !modelIds.includes(searchValue) && (
{searchValue && !modelIdsForDropdown.includes(searchValue) && (
<div className="p-1 border-t border-vscode-input-border">
<CommandItem data-testid="use-custom-model" value={searchValue} onSelect={onSelect}>
{t("settings:modelPicker.useCustomModel", { modelId: searchValue })}
@ -177,10 +190,10 @@ export const ModelPicker = ({
</PopoverContent>
</Popover>
</div>
{selectedModelId && selectedModelInfo && (
{selectedModelIdForInfo && selectedModelInfo && (
<ModelInfoView
apiProvider={apiConfiguration.apiProvider}
selectedModelId={selectedModelId}
selectedModelId={selectedModelIdForInfo}
modelInfo={selectedModelInfo}
isDescriptionExpanded={isDescriptionExpanded}
setIsDescriptionExpanded={setIsDescriptionExpanded}

View file

@ -2,12 +2,12 @@ import { useCallback } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types"
import { RouterModels, glamaDefaultModelId } from "@roo/api"
import { glamaDefaultModelId } from "@roo/api"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { getGlamaAuthUrl } from "@src/oauth/urls"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { useProviderModels } from "../../ui/hooks/useProviderModels"
import { inputEventTransform } from "../transforms"
import { ModelPicker } from "../ModelPicker"
@ -15,20 +15,15 @@ import { ModelPicker } from "../ModelPicker"
type GlamaProps = {
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
routerModels?: RouterModels
uriScheme?: string
organizationAllowList: OrganizationAllowList
}
export const Glama = ({
apiConfiguration,
setApiConfigurationField,
routerModels,
uriScheme,
organizationAllowList,
}: GlamaProps) => {
export const Glama = ({ apiConfiguration, setApiConfigurationField, uriScheme, organizationAllowList }: GlamaProps) => {
const { t } = useAppTranslation()
const { models: glamaModelsData, isLoading: isLoadingModels, error: modelsError } = useProviderModels("glama")
const handleInputChange = useCallback(
<K extends keyof ProviderSettings, E>(
field: K,
@ -40,6 +35,14 @@ export const Glama = ({
[setApiConfigurationField],
)
if (isLoadingModels) {
return <p>{t("settings:providers.refreshModels.loading")}</p>
}
if (modelsError) {
return <p className="text-vscode-errorForeground">{t("settings:providers.refreshModels.error")}</p>
}
return (
<>
<VSCodeTextField
@ -62,7 +65,7 @@ export const Glama = ({
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId={glamaDefaultModelId}
models={routerModels?.glama ?? {}}
models={glamaModelsData ?? {}}
modelIdKey="glamaModelId"
serviceName="Glama"
serviceUrl="https://glama.ai/models"

View file

@ -1,15 +1,12 @@
import { useCallback, useState, useEffect, useRef } from "react"
import { useCallback, useMemo } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types"
import { litellmDefaultModelId, RouterName } from "@roo/api"
import { ExtensionMessage } from "@roo/ExtensionMessage"
import { litellmDefaultModelId } from "@roo/api"
import { vscode } from "@src/utils/vscode"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Button } from "@src/components/ui"
import { useProviderModels } from "../../ui/hooks/useProviderModels"
import { inputEventTransform } from "../transforms"
import { ModelPicker } from "../ModelPicker"
@ -22,38 +19,22 @@ type LiteLLMProps = {
export const LiteLLM = ({ apiConfiguration, setApiConfigurationField, organizationAllowList }: LiteLLMProps) => {
const { t } = useAppTranslation()
const { routerModels } = useExtensionState()
const [refreshStatus, setRefreshStatus] = useState<"idle" | "loading" | "success" | "error">("idle")
const [refreshError, setRefreshError] = useState<string | undefined>()
const litellmErrorJustReceived = useRef(false)
useEffect(() => {
const handleMessage = (event: MessageEvent<ExtensionMessage>) => {
const message = event.data
if (message.type === "singleRouterModelFetchResponse" && !message.success) {
const providerName = message.values?.provider as RouterName
if (providerName === "litellm") {
litellmErrorJustReceived.current = true
setRefreshStatus("error")
setRefreshError(message.error)
}
} else if (message.type === "routerModels") {
// If we were loading and no specific error for litellm was just received, mark as success.
// The ModelPicker will show available models or "no models found".
if (refreshStatus === "loading") {
if (!litellmErrorJustReceived.current) {
setRefreshStatus("success")
}
// If litellmErrorJustReceived.current is true, status is already (or will be) "error".
}
}
}
const providerModelsOptions = useMemo(
() => ({
flushCacheFirst: true,
litellmApiKey: apiConfiguration?.litellmApiKey,
litellmBaseUrl: apiConfiguration?.litellmBaseUrl,
}),
[apiConfiguration?.litellmApiKey, apiConfiguration?.litellmBaseUrl],
)
window.addEventListener("message", handleMessage)
return () => {
window.removeEventListener("message", handleMessage)
}
}, [refreshStatus, refreshError, setRefreshStatus, setRefreshError])
const {
models: litellmModelsData,
isLoading: isLoadingModels,
error: modelsError,
} = useProviderModels("litellm", providerModelsOptions)
console.log("litellmModelsData1212", litellmModelsData, isLoadingModels, modelsError)
const handleInputChange = useCallback(
<K extends keyof ProviderSettings, E>(
@ -66,22 +47,6 @@ export const LiteLLM = ({ apiConfiguration, setApiConfigurationField, organizati
[setApiConfigurationField],
)
const handleRefreshModels = useCallback(() => {
litellmErrorJustReceived.current = false // Reset flag on new refresh action
setRefreshStatus("loading")
setRefreshError(undefined)
const key = apiConfiguration.litellmApiKey
const url = apiConfiguration.litellmBaseUrl
if (!key || !url) {
setRefreshStatus("error")
setRefreshError(t("settings:providers.refreshModels.missingConfig"))
return
}
vscode.postMessage({ type: "requestRouterModels", values: { litellmApiKey: key, litellmBaseUrl: url } })
}, [apiConfiguration, setRefreshStatus, setRefreshError, t])
return (
<>
<VSCodeTextField
@ -105,39 +70,18 @@ export const LiteLLM = ({ apiConfiguration, setApiConfigurationField, organizati
{t("settings:providers.apiKeyStorageNotice")}
</div>
<Button
variant="outline"
onClick={handleRefreshModels}
disabled={
refreshStatus === "loading" || !apiConfiguration.litellmApiKey || !apiConfiguration.litellmBaseUrl
}
className="w-full">
<div className="flex items-center gap-2">
{refreshStatus === "loading" ? (
<span className="codicon codicon-loading codicon-modifier-spin" />
) : (
<span className="codicon codicon-refresh" />
)}
{t("settings:providers.refreshModels.label")}
</div>
</Button>
{refreshStatus === "loading" && (
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.refreshModels.loading")}
</div>
{isLoadingModels && <p>{t("settings:providers.refreshModels.loading")}</p>}
{modelsError && (
<p className="text-vscode-errorForeground">{t("settings:providers.refreshModels.error")}</p>
)}
{refreshStatus === "success" && (
<div className="text-sm text-vscode-foreground">{t("settings:providers.refreshModels.success")}</div>
)}
{refreshStatus === "error" && (
<div className="text-sm text-vscode-errorForeground">
{refreshError || t("settings:providers.refreshModels.error")}
</div>
{!isLoadingModels && !modelsError && litellmModelsData && Object.keys(litellmModelsData).length === 0 && (
<p>{t("settings:common.noModelsFound")}</p>
)}
<ModelPicker
apiConfiguration={apiConfiguration}
defaultModelId={litellmDefaultModelId}
models={routerModels?.litellm ?? {}}
models={litellmModelsData ?? null}
modelIdKey="litellmModelId"
serviceName="LiteLLM"
serviceUrl="https://docs.litellm.ai/"

View file

@ -1,13 +1,10 @@
import { useState, useCallback } from "react"
import { useEvent } from "react-use"
import { useCallback } from "react"
import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
import { ExtensionMessage } from "@roo/ExtensionMessage"
import { useProviderModels } from "../../ui/hooks/useProviderModels"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { inputEventTransform } from "../transforms"
type OllamaProps = {
@ -18,7 +15,7 @@ type OllamaProps = {
export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaProps) => {
const { t } = useAppTranslation()
const [ollamaModels, setOllamaModels] = useState<string[]>([])
const { models: ollamaModelsData, isLoading: isLoadingModels, error: modelsError } = useProviderModels("ollama")
const handleInputChange = useCallback(
<K extends keyof ProviderSettings, E>(
@ -31,20 +28,19 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
[setApiConfigurationField],
)
const onMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (isLoadingModels) {
return <div className="p-2 text-sm text-vscode-descriptionForeground">{t("settings:common.loadingModels")}</div>
}
switch (message.type) {
case "ollamaModels":
{
const newModels = message.ollamaModels ?? []
setOllamaModels(newModels)
}
break
}
}, [])
if (modelsError) {
return (
<div className="p-2 text-sm text-vscode-errorForeground">
{t("settings:common.errorModels")}: {modelsError}
</div>
)
}
useEvent("message", onMessage)
const availableModelIds = ollamaModelsData ? Object.keys(ollamaModelsData) : []
return (
<>
@ -63,17 +59,27 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.ollama.modelId")}</label>
</VSCodeTextField>
{ollamaModels.length > 0 && (
{!isLoadingModels && !modelsError && availableModelIds.length === 0 && (
<div className="p-2 text-sm text-vscode-descriptionForeground">
{t("settings:common.noModelsFound")}
</div>
)}
{availableModelIds.length > 0 && (
<VSCodeRadioGroup
value={
ollamaModels.includes(apiConfiguration?.ollamaModelId || "")
availableModelIds.includes(apiConfiguration?.ollamaModelId || "")
? apiConfiguration?.ollamaModelId
: ""
}
onChange={handleInputChange("ollamaModelId")}>
{ollamaModels.map((model) => (
<VSCodeRadio key={model} value={model} checked={apiConfiguration?.ollamaModelId === model}>
{model}
{availableModelIds.map((modelId) => (
<VSCodeRadio
key={modelId}
value={modelId}
checked={apiConfiguration?.ollamaModelId === modelId}>
{modelId}
</VSCodeRadio>
))}
</VSCodeRadioGroup>

View file

@ -1,15 +1,14 @@
import { useState, useCallback, useEffect } from "react"
import { useEvent } from "react-use"
import { Checkbox } from "vscrui"
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings, ModelInfo, ReasoningEffort, OrganizationAllowList } from "@roo-code/types"
import type { ProviderSettings, ReasoningEffort, OrganizationAllowList } from "@roo-code/types"
import { azureOpenAiDefaultApiVersion, openAiModelInfoSaneDefaults } from "@roo/api"
import { ExtensionMessage } from "@roo/ExtensionMessage"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Button } from "@src/components/ui"
import { useProviderModels } from "@src/components/ui/hooks/useProviderModels"
import { convertHeadersToObject } from "../utils/headers"
import { inputEventTransform, noTransform } from "../transforms"
@ -33,7 +32,11 @@ export const OpenAICompatible = ({
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
const [openAiLegacyFormatSelected, setOpenAiLegacyFormatSelected] = useState(!!apiConfiguration?.openAiLegacyFormat)
const [openAiModels, setOpenAiModels] = useState<Record<string, ModelInfo> | null>(null)
const {
models: openAiCompatibleModels,
isLoading: isLoadingOpenAiCompatibleModels,
error: openAiCompatibleModelsError,
} = useProviderModels("openai-compatible")
const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
const headers = apiConfiguration?.openAiHeaders || {}
@ -97,20 +100,6 @@ export const OpenAICompatible = ({
[setApiConfigurationField],
)
const onMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "openAiModels": {
const updatedModels = message.openAiModels ?? []
setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
break
}
}
}, [])
useEvent("message", onMessage)
return (
<>
<VSCodeTextField
@ -129,11 +118,15 @@ export const OpenAICompatible = ({
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.openAiApiKey")}</label>
</VSCodeTextField>
{isLoadingOpenAiCompatibleModels && <p>{t("settings:providers.refreshModels.loading")}</p>}
{openAiCompatibleModelsError && (
<p className="text-vscode-errorForeground">{t("settings:providers.refreshModels.error")}</p>
)}
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId="gpt-4o"
models={openAiModels}
models={openAiCompatibleModels ?? null}
modelIdKey="openAiModelId"
serviceName="OpenAI"
serviceUrl="https://platform.openai.com"

View file

@ -6,7 +6,7 @@ import { ExternalLinkIcon } from "@radix-ui/react-icons"
import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types"
import { RouterModels, openRouterDefaultModelId } from "@roo/api"
import { openRouterDefaultModelId } from "@roo/api"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { getOpenRouterAuthUrl } from "@src/oauth/urls"
@ -16,16 +16,15 @@ import {
} from "@src/components/ui/hooks/useOpenRouterModelProviders"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
import { useProviderModels } from "../../ui/hooks/useProviderModels"
import { inputEventTransform, noTransform } from "../transforms"
import { ModelPicker } from "../ModelPicker"
import { OpenRouterBalanceDisplay } from "./OpenRouterBalanceDisplay"
type OpenRouterProps = {
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
routerModels?: RouterModels
selectedModelId: string
uriScheme: string | undefined
fromWelcomeView?: boolean
@ -35,7 +34,6 @@ type OpenRouterProps = {
export const OpenRouter = ({
apiConfiguration,
setApiConfigurationField,
routerModels,
selectedModelId,
uriScheme,
fromWelcomeView,
@ -43,6 +41,12 @@ export const OpenRouter = ({
}: OpenRouterProps) => {
const { t } = useAppTranslation()
const {
models: openRouterModelsData,
isLoading: isLoadingModels,
error: modelsError,
} = useProviderModels("openrouter")
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
const handleInputChange = useCallback(
@ -56,14 +60,26 @@ export const OpenRouter = ({
[setApiConfigurationField],
)
const { data: openRouterModelProviders } = useOpenRouterModelProviders(apiConfiguration?.openRouterModelId, {
const { data: openRouterModelProviders } = useOpenRouterModelProviders(selectedModelId, {
enabled:
!!apiConfiguration?.openRouterModelId &&
routerModels?.openrouter &&
Object.keys(routerModels.openrouter).length > 1 &&
apiConfiguration.openRouterModelId in routerModels.openrouter,
!!selectedModelId &&
!!openRouterModelsData &&
Object.keys(openRouterModelsData).length > 0 &&
selectedModelId in openRouterModelsData,
})
if (isLoadingModels) {
return <div className="p-2 text-sm text-vscode-descriptionForeground">{t("settings:common.loadingModels")}</div>
}
if (modelsError) {
return (
<div className="p-2 text-sm text-vscode-errorForeground">
{t("settings:common.errorModels")}: {modelsError}
</div>
)
}
return (
<>
<VSCodeTextField
@ -120,7 +136,13 @@ export const OpenRouter = ({
<Trans
i18nKey="settings:providers.openRouterTransformsText"
components={{
a: <a href="https://openrouter.ai/docs/transforms" />,
_blank: (
<a
href="https://openrouter.ai/docs/transforms"
target="_blank"
rel="noopener noreferrer"
/>
),
}}
/>
</Checkbox>
@ -130,7 +152,7 @@ export const OpenRouter = ({
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId={openRouterDefaultModelId}
models={routerModels?.openrouter ?? {}}
models={openRouterModelsData ?? {}}
modelIdKey="openRouterModelId"
serviceName="OpenRouter"
serviceUrl="https://openrouter.ai/models"
@ -142,7 +164,10 @@ export const OpenRouter = ({
<label className="block font-medium mb-1">
{t("settings:providers.openRouter.providerRouting.title")}
</label>
<a href={`https://openrouter.ai/${selectedModelId}/providers`}>
<a
href={`https://openrouter.ai/${selectedModelId}/providers`}
target="_blank"
rel="noopener noreferrer">
<ExternalLinkIcon className="w-4 h-4" />
</a>
</div>
@ -165,7 +190,10 @@ export const OpenRouter = ({
</Select>
<div className="text-sm text-vscode-descriptionForeground mt-1">
{t("settings:providers.openRouter.providerRouting.description")}{" "}
<a href="https://openrouter.ai/docs/features/provider-routing">
<a
href="https://openrouter.ai/docs/features/provider-routing"
target="_blank"
rel="noopener noreferrer">
{t("settings:providers.openRouter.providerRouting.learnMore")}.
</a>
</div>

View file

@ -1,14 +1,13 @@
import { useCallback, useState } from "react"
import { useCallback } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types"
import { RouterModels, requestyDefaultModelId } from "@roo/api"
import { requestyDefaultModelId } from "@roo/api"
import { vscode } from "@src/utils/vscode"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { Button } from "@src/components/ui"
import { useProviderModels } from "../../ui/hooks/useProviderModels"
import { inputEventTransform } from "../transforms"
import { ModelPicker } from "../ModelPicker"
@ -17,21 +16,14 @@ import { RequestyBalanceDisplay } from "./RequestyBalanceDisplay"
type RequestyProps = {
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
routerModels?: RouterModels
refetchRouterModels: () => void
organizationAllowList: OrganizationAllowList
}
export const Requesty = ({
apiConfiguration,
setApiConfigurationField,
routerModels,
refetchRouterModels,
organizationAllowList,
}: RequestyProps) => {
export const Requesty = ({ apiConfiguration, setApiConfigurationField, organizationAllowList }: RequestyProps) => {
const { t } = useAppTranslation()
const [didRefetch, setDidRefetch] = useState<boolean>()
const { models: requestyModelsData, isLoading: isLoadingModels, error: modelsError } = useProviderModels("requesty")
const handleInputChange = useCallback(
<K extends keyof ProviderSettings, E>(
@ -44,6 +36,18 @@ export const Requesty = ({
[setApiConfigurationField],
)
if (isLoadingModels) {
return <div className="p-2 text-sm text-vscode-descriptionForeground">{t("settings:common.loadingModels")}</div>
}
if (modelsError) {
return (
<div className="p-2 text-sm text-vscode-errorForeground">
{t("settings:common.errorModels")}: {modelsError}
</div>
)
}
return (
<>
<VSCodeTextField
@ -70,28 +74,11 @@ export const Requesty = ({
{t("settings:providers.getRequestyApiKey")}
</VSCodeButtonLink>
)}
<Button
variant="outline"
onClick={() => {
vscode.postMessage({ type: "flushRouterModels", text: "requesty" })
refetchRouterModels()
setDidRefetch(true)
}}>
<div className="flex items-center gap-2">
<span className="codicon codicon-refresh" />
{t("settings:providers.refreshModels.label")}
</div>
</Button>
{didRefetch && (
<div className="flex items-center text-vscode-errorForeground">
{t("settings:providers.refreshModels.hint")}
</div>
)}
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId={requestyDefaultModelId}
models={routerModels?.requesty ?? {}}
models={requestyModelsData ?? {}}
modelIdKey="requestyModelId"
serviceName="Requesty"
serviceUrl="https://requesty.ai"

View file

@ -1,15 +1,12 @@
import { useCallback, useState, useRef } from "react"
import { useCallback, useState, useEffect, useRef } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useQueryClient } from "@tanstack/react-query"
import type { ProviderSettings, OrganizationAllowList } from "@roo-code/types"
import { RouterModels, unboundDefaultModelId } from "@roo/api"
import { unboundDefaultModelId } from "@roo/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 { useProviderModels } from "../../ui/hooks/useProviderModels"
import { inputEventTransform } from "../transforms"
import { ModelPicker } from "../ModelPicker"
@ -17,23 +14,15 @@ import { ModelPicker } from "../ModelPicker"
type UnboundProps = {
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
routerModels?: RouterModels
organizationAllowList: OrganizationAllowList
}
export const Unbound = ({
apiConfiguration,
setApiConfigurationField,
routerModels,
organizationAllowList,
}: UnboundProps) => {
export const Unbound = ({ apiConfiguration, setApiConfigurationField, organizationAllowList }: 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 { models: unboundModelsData, isLoading: isLoadingModels, error: modelsError } = useProviderModels("unbound")
const [isInvalidKeyFeedback, setIsInvalidKeyFeedback] = useState<boolean>(false)
const invalidKeyTimerRef = useRef<NodeJS.Timeout>()
const handleInputChange = useCallback(
@ -43,93 +32,42 @@ export const Unbound = ({
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
if (field === "unboundApiKey") {
setIsInvalidKeyFeedback(false)
if (invalidKeyTimerRef.current) clearTimeout(invalidKeyTimerRef.current)
}
},
[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
useEffect(() => {
if (
modelsError &&
(modelsError.includes("401") ||
modelsError.toLowerCase().includes("unauthorized") ||
modelsError.toLowerCase().includes("invalid api key"))
) {
setIsInvalidKeyFeedback(true)
invalidKeyTimerRef.current = setTimeout(() => setIsInvalidKeyFeedback(false), 5000)
} else {
return true
setIsInvalidKeyFeedback(false)
}
}, [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)
return () => {
if (invalidKeyTimerRef.current) clearTimeout(invalidKeyTimerRef.current)
}
}, [saveConfiguration, requestModels])
}, [modelsError])
if (isLoadingModels && !unboundModelsData) {
return <div className="p-2 text-sm text-vscode-descriptionForeground">{t("settings:common.loadingModels")}</div>
}
if (modelsError && !isInvalidKeyFeedback) {
return (
<div className="p-2 text-sm text-vscode-errorForeground">
{t("settings:common.errorModels")}: {modelsError}
</div>
)
}
return (
<>
@ -149,28 +87,15 @@ export const Unbound = ({
{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">
{isInvalidKeyFeedback && (
<div className="flex items-center text-vscode-errorForeground mt-1">
{t("settings:providers.unboundInvalidApiKey")}
</div>
)}
<ModelPicker
apiConfiguration={apiConfiguration}
defaultModelId={unboundDefaultModelId}
models={routerModels?.unbound ?? {}}
models={unboundModelsData ?? {}}
modelIdKey="unboundModelId"
serviceName="Unbound"
serviceUrl="https://api.getunbound.ai/models"

View file

@ -1,16 +1,12 @@
import { useState, useCallback } from "react"
import { useEvent } from "react-use"
import { useCallback } from "react"
import { LanguageModelChatSelector } from "vscode"
import type { ProviderSettings } from "@roo-code/types"
import { ExtensionMessage } from "@roo/ExtensionMessage"
import { useProviderModels } from "../../ui/hooks/useProviderModels"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
import { inputEventTransform } from "../transforms"
type VSCodeLMProps = {
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
@ -19,69 +15,90 @@ type VSCodeLMProps = {
export const VSCodeLM = ({ apiConfiguration, setApiConfigurationField }: VSCodeLMProps) => {
const { t } = useAppTranslation()
const [vsCodeLmModels, setVsCodeLmModels] = useState<LanguageModelChatSelector[]>([])
const { models: vsCodeLmModelsData, isLoading: isLoadingModels, error: modelsError } = useProviderModels("vscodelm")
const handleInputChange = useCallback(
<K extends keyof ProviderSettings, E>(
field: K,
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
const handleModelSelectionChange = useCallback(
(selectedModelId: string) => {
const modelInfo = vsCodeLmModelsData?.[selectedModelId]
let selector: LanguageModelChatSelector = { id: selectedModelId }
if (modelInfo && typeof modelInfo.description === "string") {
const vendorMatch = modelInfo.description.match(/Vendor: ([^,]+)/)
const familyMatch = modelInfo.description.match(/Family: ([^,)]+)/)
if (vendorMatch?.[1] && familyMatch?.[1]) {
selector = { vendor: vendorMatch[1].trim(), family: familyMatch[1].trim(), id: selectedModelId }
} else if (selectedModelId.includes("/")) {
const parts = selectedModelId.split("/")
if (parts.length >= 2) {
selector = { vendor: parts[0], family: parts[1], id: selectedModelId }
if (parts.length >= 3) selector.version = parts[2]
}
}
}
setApiConfigurationField("vsCodeLmModelSelector", selector)
},
[setApiConfigurationField, vsCodeLmModelsData],
)
const onMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (isLoadingModels) {
return <div className="p-2 text-sm text-vscode-descriptionForeground">{t("settings:common.loadingModels")}</div>
}
switch (message.type) {
case "vsCodeLmModels":
{
const newModels = message.vsCodeLmModels ?? []
setVsCodeLmModels(newModels)
}
break
if (modelsError) {
return (
<div className="p-2 text-sm text-vscode-errorForeground">
{t("settings:common.errorModels")}: {modelsError}
</div>
)
}
const availableModels = vsCodeLmModelsData ? Object.entries(vsCodeLmModelsData) : []
let currentSelectedValue = ""
const currentSelector = apiConfiguration?.vsCodeLmModelSelector
if (currentSelector) {
if (currentSelector.id && availableModels.some(([id]) => id === currentSelector.id)) {
currentSelectedValue = currentSelector.id
} else if (currentSelector.vendor && currentSelector.family) {
const constructedId = `${currentSelector.vendor}/${currentSelector.family}`.toLowerCase()
if (availableModels.some(([id]) => id.startsWith(constructedId))) {
currentSelectedValue = availableModels.find(([id]) => id.startsWith(constructedId))?.[0] || ""
}
}
}, [])
useEvent("message", onMessage)
}
if (!currentSelectedValue && availableModels.length > 0) {
// If still no value and models exist, maybe pick the first one or default?
// For now, leave as empty string if no match from config.
}
return (
<>
<div>
<label className="block font-medium mb-1">{t("settings:providers.vscodeLmModel")}</label>
{vsCodeLmModels.length > 0 ? (
<Select
value={
apiConfiguration?.vsCodeLmModelSelector
? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}`
: ""
}
onValueChange={handleInputChange("vsCodeLmModelSelector", (value) => {
const [vendor, family] = value.split("/")
return { vendor, family }
})}>
{availableModels.length > 0 ? (
<Select value={currentSelectedValue} onValueChange={handleModelSelectionChange}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
{vsCodeLmModels.map((model) => (
<SelectItem
key={`${model.vendor}/${model.family}`}
value={`${model.vendor}/${model.family}`}>
{`${model.vendor} - ${model.family}`}
{availableModels.map(([id, modelInfo]) => (
<SelectItem key={id} value={id}>
{modelInfo?.description || id}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.vscodeLmDescription")}
{isLoadingModels
? t("settings:common.loadingModels")
: t("settings:providers.vscodeLmDescription")}
</div>
)}
</div>
<div className="text-sm text-vscode-errorForeground">{t("settings:providers.vscodeLmWarning")}</div>
<div className="text-sm text-vscode-errorForeground mt-1">{t("settings:providers.vscodeLmWarning")}</div>
</>
)
}

View file

@ -0,0 +1,180 @@
import { useQuery, useQueryClient, QueryKey } from "@tanstack/react-query"
import { useEffect, useMemo, useRef, useState } from "react"
import { RouterName, ModelRecord } from "@roo/api"
import { ExtensionMessage } from "@roo/ExtensionMessage"
import { vscode } from "@src/utils/vscode"
import { useDebounceEffect } from "@src/utils/useDebounceEffect"
// --- START: Type definitions for provider-specific params ---
// Inspired by GetModelsOptions from src/shared/api.ts
// These are the *additional* params a provider might need, sent from the UI.
export type ProviderSpecificParamsMap = {
openrouter: Record<string, never>
glama: Record<string, never>
requesty: { requestyApiKey?: string }
unbound: { unboundApiKey?: string }
litellm: { litellmApiKey?: string; litellmBaseUrl?: string }
ollama: { baseUrl?: string }
lmstudio: { baseUrl?: string }
vscodelm: Record<string, never>
"openai-compatible": {
baseUrl: string
apiKey?: string
openAiHeaders?: Record<string, string>
}
}
// The options object for useProviderModels hook and fetchProviderModels function
export type UseProviderModelsOptions<P extends RouterName> = {
flushCacheFirst?: boolean
} & ProviderSpecificParamsMap[P]
// --- END: Type definitions for provider-specific params ---
interface UseProviderModelsResult {
models?: ModelRecord
isLoading: boolean
error?: string
refetch: () => void
}
const DEBOUNCE_DELAY = 250
const REQUEST_TIMEOUT = 15000
const fetchProviderModels = async <P extends RouterName>(
providerName: P,
options?: UseProviderModelsOptions<P>,
): Promise<ModelRecord> => {
// Use AbortController for better cleanup
const abortController = new AbortController()
return new Promise<ModelRecord>((resolve, reject) => {
let handler: ((event: MessageEvent) => void) | null = null
let timeoutId: NodeJS.Timeout | null = null
const cleanup = () => {
if (handler) {
window.removeEventListener("message", handler)
handler = null
}
if (timeoutId) {
clearTimeout(timeoutId)
timeoutId = null
}
}
// Set up timeout
timeoutId = setTimeout(() => {
cleanup()
reject(new Error(`Request for ${providerName} models timed out`))
}, REQUEST_TIMEOUT)
// Set up message handler
handler = (event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (message.type === "singleRouterModelFetchResponse" && message.values?.provider === providerName) {
cleanup()
if (message.success && message.values?.models) {
resolve(message.values.models as ModelRecord)
} else {
reject(new Error(message.error || `Failed to fetch models for ${providerName}`))
}
}
}
// Listen for abort signal
abortController.signal.addEventListener("abort", () => {
cleanup()
reject(new Error("Request was aborted"))
})
window.addEventListener("message", handler)
const { flushCacheFirst = true, ...providerParams } = options || {}
vscode.postMessage({
type: "requestRouterModels",
values: { provider: providerName, flushCacheFirst, ...providerParams },
})
})
}
export const useProviderModels = <P extends RouterName>(
providerName: P,
options?: UseProviderModelsOptions<P>,
): UseProviderModelsResult => {
const queryClient = useQueryClient()
// Track if we're currently debouncing
const debouncingRef = useRef(false)
const [debouncedReady, setDebouncedReady] = useState(false)
// Debounce the options to avoid rapid re-fetches
const [debouncedOptions, setDebouncedOptions] = useState(options)
// Extract relevant options for debouncing (exclude flushCacheFirst)
const { flushCacheFirst: _flush, ...relevantOptions } = options || {}
const optionsKey = JSON.stringify({ providerName, ...relevantOptions })
// Reset debouncing state when options change
useEffect(() => {
debouncingRef.current = true
setDebouncedReady(false)
}, [optionsKey])
// Debounce the options update
useDebounceEffect(
() => {
setDebouncedOptions(options)
debouncingRef.current = false
setDebouncedReady(true)
},
DEBOUNCE_DELAY,
[options, providerName],
)
// Create a stable query key based on debounced options
const queryKey: QueryKey = useMemo(
() => ["providerModels", providerName, debouncedOptions || {}],
[providerName, debouncedOptions],
)
// Query for provider models
const {
data,
isLoading: isQueryLoading,
error: queryError,
refetch,
} = useQuery<ModelRecord, Error>({
queryKey,
queryFn: () => fetchProviderModels(providerName, debouncedOptions),
enabled: !!providerName && debouncedReady,
retry: false,
staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
})
// Listen for cache invalidation messages
useEffect(() => {
const handler = (event: MessageEvent) => {
const message: ExtensionMessage = event.data
if ((message.type as any) === "flushRouterModels" && message?.values?.provider === providerName) {
queryClient.invalidateQueries({ queryKey })
}
}
window.addEventListener("message", handler)
return () => window.removeEventListener("message", handler)
}, [providerName, queryClient, queryKey])
// Combine debouncing and query loading states
const isLoading = debouncingRef.current || isQueryLoading
// Clear error when in loading state
const error = isLoading ? undefined : queryError?.message
return {
models: data,
isLoading,
error,
refetch,
}
}

View file

@ -1,38 +0,0 @@
import { useQuery } from "@tanstack/react-query"
import { RouterModels } from "@roo/api"
import { ExtensionMessage } from "@roo/ExtensionMessage"
import { vscode } from "@src/utils/vscode"
const getRouterModels = async () =>
new Promise<RouterModels>((resolve, reject) => {
const cleanup = () => {
window.removeEventListener("message", handler)
}
const timeout = setTimeout(() => {
cleanup()
reject(new Error("Router models request timed out"))
}, 10000)
const handler = (event: MessageEvent) => {
const message: ExtensionMessage = event.data
if (message.type === "routerModels") {
clearTimeout(timeout)
cleanup()
if (message.routerModels) {
resolve(message.routerModels)
} else {
reject(new Error("No router models in response"))
}
}
}
window.addEventListener("message", handler)
vscode.postMessage({ type: "requestRouterModels" })
})
export const useRouterModels = () => useQuery({ queryKey: ["routerModels"], queryFn: getRouterModels })

View file

@ -1,7 +1,8 @@
import type { ProviderName, ProviderSettings, ModelInfo } from "@roo-code/types"
import {
RouterModels,
RouterName,
ModelRecord,
anthropicDefaultModelId,
anthropicModels,
bedrockDefaultModelId,
@ -25,186 +26,199 @@ import {
chutesDefaultModelId,
vscodeLlmModels,
vscodeLlmDefaultModelId,
VscodeLlmModelId,
openRouterDefaultModelId,
requestyDefaultModelId,
glamaDefaultModelId,
unboundDefaultModelId,
litellmDefaultModelId,
isRouterName,
} from "@roo/api"
import { useRouterModels } from "./useRouterModels"
import { useProviderModels } from "./useProviderModels"
import { useOpenRouterModelProviders } from "./useOpenRouterModelProviders"
export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
const provider = apiConfiguration?.apiProvider || "anthropic"
const openRouterModelId = provider === "openrouter" ? apiConfiguration?.openRouterModelId : undefined
const routerModels = useRouterModels()
const currentProviderIsRouter = isRouterName(provider)
const {
models: routerProviderModels,
isLoading: isRouterProviderLoading,
error: routerProviderError,
} = useProviderModels(currentProviderIsRouter ? (provider as RouterName) : undefined)
const openRouterModelProviders = useOpenRouterModelProviders(openRouterModelId)
const { id, info } =
apiConfiguration &&
typeof routerModels.data !== "undefined" &&
typeof openRouterModelProviders.data !== "undefined"
? getSelectedModel({
provider,
apiConfiguration,
routerModels: routerModels.data,
openRouterModelProviders: openRouterModelProviders.data,
})
: { id: anthropicDefaultModelId, info: undefined }
const { id, info } = (() => {
if (!apiConfiguration) {
return { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] }
}
if (currentProviderIsRouter && (isRouterProviderLoading || routerProviderError)) {
return { id: isRouterProviderLoading ? "loading..." : "error", info: undefined }
}
if (provider === "openrouter" && (openRouterModelProviders.isLoading || openRouterModelProviders.isError)) {
return { id: openRouterModelProviders.isLoading ? "loading..." : "error", info: undefined }
}
return getSelectedModel({
provider,
apiConfiguration,
providerModelRecord: currentProviderIsRouter ? routerProviderModels : undefined,
openRouterModelProviders: openRouterModelProviders.data,
})
})()
return {
provider,
id,
info,
isLoading: routerModels.isLoading || openRouterModelProviders.isLoading,
isError: routerModels.isError || openRouterModelProviders.isError,
isLoading:
(currentProviderIsRouter && isRouterProviderLoading) ||
(provider === "openrouter" && openRouterModelProviders.isLoading),
isError:
!!(currentProviderIsRouter && routerProviderError) ||
(provider === "openrouter" && openRouterModelProviders.isError),
}
}
function getSelectedModel({
provider,
apiConfiguration,
routerModels,
providerModelRecord,
openRouterModelProviders,
}: {
provider: ProviderName
apiConfiguration: ProviderSettings
routerModels: RouterModels
openRouterModelProviders: Record<string, ModelInfo>
}): { id: string; info: ModelInfo } {
providerModelRecord?: ModelRecord
openRouterModelProviders?: Record<string, ModelInfo>
}): { id: string; info?: ModelInfo } {
switch (provider) {
case "openrouter": {
const id = apiConfiguration.openRouterModelId ?? openRouterDefaultModelId
let info = routerModels.openrouter[id]
let modelInfo = providerModelRecord?.[id]
const specificProvider = apiConfiguration.openRouterSpecificProvider
if (specificProvider && openRouterModelProviders[specificProvider]) {
// Overwrite the info with the specific provider info. Some
// fields are missing the model info for `openRouterModelProviders`
// so we need to merge the two.
info = info
? { ...info, ...openRouterModelProviders[specificProvider] }
if (specificProvider && openRouterModelProviders?.[specificProvider]) {
modelInfo = modelInfo
? { ...modelInfo, ...openRouterModelProviders[specificProvider] }
: openRouterModelProviders[specificProvider]
}
return info
? { id, info }
: { id: openRouterDefaultModelId, info: routerModels.openrouter[openRouterDefaultModelId] }
return { id, info: modelInfo || providerModelRecord?.[openRouterDefaultModelId] }
}
case "requesty": {
const id = apiConfiguration.requestyModelId ?? requestyDefaultModelId
const info = routerModels.requesty[id]
return info
? { id, info }
: { id: requestyDefaultModelId, info: routerModels.requesty[requestyDefaultModelId] }
return { id, info: providerModelRecord?.[id] || providerModelRecord?.[requestyDefaultModelId] }
}
case "glama": {
const id = apiConfiguration.glamaModelId ?? glamaDefaultModelId
const info = routerModels.glama[id]
return info ? { id, info } : { id: glamaDefaultModelId, info: routerModels.glama[glamaDefaultModelId] }
return { id, info: providerModelRecord?.[id] || providerModelRecord?.[glamaDefaultModelId] }
}
case "unbound": {
const id = apiConfiguration.unboundModelId ?? unboundDefaultModelId
const info = routerModels.unbound[id]
return info
? { id, info }
: { id: unboundDefaultModelId, info: routerModels.unbound[unboundDefaultModelId] }
return { id, info: providerModelRecord?.[id] || providerModelRecord?.[unboundDefaultModelId] }
}
case "litellm": {
const id = apiConfiguration.litellmModelId ?? litellmDefaultModelId
const info = routerModels.litellm[id]
return info
? { id, info }
: { id: litellmDefaultModelId, info: routerModels.litellm[litellmDefaultModelId] }
return { id, info: providerModelRecord?.[id] || providerModelRecord?.[litellmDefaultModelId] }
}
case "ollama": {
const id = apiConfiguration.ollamaModelId ?? ""
return { id, info: providerModelRecord?.[id] || openAiModelInfoSaneDefaults }
}
case "lmstudio": {
const id = apiConfiguration.lmStudioModelId ?? ""
return { id, info: providerModelRecord?.[id] || openAiModelInfoSaneDefaults }
}
case "vscode-lm": {
const selector = apiConfiguration.vsCodeLmModelSelector
let selectedModelId: string
if (selector && selector.id) {
selectedModelId = selector.id
} else if (selector && selector.vendor && selector.family) {
selectedModelId = `${selector.vendor}/${selector.family}`.toLowerCase()
} else {
selectedModelId = vscodeLlmDefaultModelId
}
let modelInfo = providerModelRecord?.[selectedModelId]
if (!modelInfo) {
modelInfo = providerModelRecord?.[vscodeLlmDefaultModelId]
}
if (!modelInfo) {
modelInfo = vscodeLlmModels[vscodeLlmDefaultModelId as VscodeLlmModelId]
}
return {
id: selectedModelId,
info: { ...openAiModelInfoSaneDefaults, ...modelInfo, supportsImages: false },
}
}
case "xai": {
const id = apiConfiguration.apiModelId ?? xaiDefaultModelId
const info = xaiModels[id as keyof typeof xaiModels]
return info ? { id, info } : { id: xaiDefaultModelId, info: xaiModels[xaiDefaultModelId] }
return { id, info: info || xaiModels[xaiDefaultModelId] }
}
case "groq": {
const id = apiConfiguration.apiModelId ?? groqDefaultModelId
const info = groqModels[id as keyof typeof groqModels]
return info ? { id, info } : { id: groqDefaultModelId, info: groqModels[groqDefaultModelId] }
return { id, info: info || groqModels[groqDefaultModelId] }
}
case "chutes": {
const id = apiConfiguration.apiModelId ?? chutesDefaultModelId
const info = chutesModels[id as keyof typeof chutesModels]
return info ? { id, info } : { id: chutesDefaultModelId, info: chutesModels[chutesDefaultModelId] }
return { id, info: info || chutesModels[chutesDefaultModelId] }
}
case "bedrock": {
const id = apiConfiguration.apiModelId ?? bedrockDefaultModelId
const info = bedrockModels[id as keyof typeof bedrockModels]
// Special case for custom ARN.
if (id === "custom-arn") {
return {
id,
info: { maxTokens: 5000, contextWindow: 128_000, supportsPromptCache: false, supportsImages: true },
}
}
return info ? { id, info } : { id: bedrockDefaultModelId, info: bedrockModels[bedrockDefaultModelId] }
const info = bedrockModels[id as keyof typeof bedrockModels]
return { id, info: info || bedrockModels[bedrockDefaultModelId] }
}
case "vertex": {
const id = apiConfiguration.apiModelId ?? vertexDefaultModelId
const info = vertexModels[id as keyof typeof vertexModels]
return info ? { id, info } : { id: vertexDefaultModelId, info: vertexModels[vertexDefaultModelId] }
return { id, info: info || vertexModels[vertexDefaultModelId] }
}
case "gemini": {
const id = apiConfiguration.apiModelId ?? geminiDefaultModelId
const info = geminiModels[id as keyof typeof geminiModels]
return info ? { id, info } : { id: geminiDefaultModelId, info: geminiModels[geminiDefaultModelId] }
return { id, info: info || geminiModels[geminiDefaultModelId] }
}
case "deepseek": {
const id = apiConfiguration.apiModelId ?? deepSeekDefaultModelId
const info = deepSeekModels[id as keyof typeof deepSeekModels]
return info ? { id, info } : { id: deepSeekDefaultModelId, info: deepSeekModels[deepSeekDefaultModelId] }
return { id, info: info || deepSeekModels[deepSeekDefaultModelId] }
}
case "openai-native": {
const id = apiConfiguration.apiModelId ?? openAiNativeDefaultModelId
const info = openAiNativeModels[id as keyof typeof openAiNativeModels]
return info
? { id, info }
: { id: openAiNativeDefaultModelId, info: openAiNativeModels[openAiNativeDefaultModelId] }
return { id, info: info || openAiNativeModels[openAiNativeDefaultModelId] }
}
case "mistral": {
const id = apiConfiguration.apiModelId ?? mistralDefaultModelId
const info = mistralModels[id as keyof typeof mistralModels]
return info ? { id, info } : { id: mistralDefaultModelId, info: mistralModels[mistralDefaultModelId] }
return { id, info: info || mistralModels[mistralDefaultModelId] }
}
case "openai": {
const id = apiConfiguration.openAiModelId ?? ""
const info = apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults
return { id, info }
return { id, info: apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults }
}
case "ollama": {
const id = apiConfiguration.ollamaModelId ?? ""
const info = openAiModelInfoSaneDefaults
return { id, info }
}
case "lmstudio": {
const id = apiConfiguration.lmStudioModelId ?? ""
const info = openAiModelInfoSaneDefaults
return { id, info }
}
case "vscode-lm": {
const id = apiConfiguration?.vsCodeLmModelSelector
? `${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}`
: vscodeLlmDefaultModelId
const modelFamily = apiConfiguration?.vsCodeLmModelSelector?.family ?? vscodeLlmDefaultModelId
const info = vscodeLlmModels[modelFamily as keyof typeof vscodeLlmModels]
return { id, info: { ...openAiModelInfoSaneDefaults, ...info, supportsImages: false } } // VSCode LM API currently doesn't support images.
}
// case "anthropic":
// case "human-relay":
// case "fake-ai":
default: {
const id = apiConfiguration.apiModelId ?? anthropicDefaultModelId
const info = anthropicModels[id as keyof typeof anthropicModels]
return info ? { id, info } : { id: anthropicDefaultModelId, info: anthropicModels[anthropicDefaultModelId] }
return { id, info: info || anthropicModels[anthropicDefaultModelId] }
}
}
}

View file

@ -19,7 +19,6 @@ import { Mode, defaultModeSlug, defaultPrompts } from "@roo/modes"
import { CustomSupportPrompts } from "@roo/support-prompt"
import { experimentDefault } from "@roo/experiments"
import { TelemetrySetting } from "@roo/TelemetrySetting"
import { RouterModels } from "@roo/api"
import { vscode } from "@src/utils/vscode"
import { convertTextMateToHljs } from "@src/utils/textMateToHljs"
@ -115,7 +114,6 @@ export interface ExtensionStateContextType extends ExtensionState {
setAutoCondenseContext: (value: boolean) => void
autoCondenseContextPercent: number
setAutoCondenseContextPercent: (value: number) => void
routerModels?: RouterModels
}
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@ -217,7 +215,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
const [openedTabs, setOpenedTabs] = useState<Array<{ label: string; isActive: boolean; path?: string }>>([])
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
const [currentCheckpoint, setCurrentCheckpoint] = useState<string>()
const [extensionRouterModels, setExtensionRouterModels] = useState<RouterModels | undefined>(undefined)
const setListApiConfigMeta = useCallback(
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
@ -285,10 +282,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setListApiConfigMeta(message.listApiConfig ?? [])
break
}
case "routerModels": {
setExtensionRouterModels(message.routerModels)
break
}
}
},
[setListApiConfigMeta],
@ -314,7 +307,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
writeDelayMs: state.writeDelayMs,
screenshotQuality: state.screenshotQuality,
routerModels: extensionRouterModels,
setExperimentEnabled: (id, enabled) =>
setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })),
setApiConfiguration,

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Clau API de Requesty",
"refreshModels": {
"label": "Actualitzar models",
"noModelsFound": "No s'han trobat models. Si us plau, torneu-ho a provar.",
"hint": "Si us plau, torneu a obrir la configuració per veure els models més recents.",
"loading": "Actualitzant la llista de models...",
"success": "Llista de models actualitzada correctament!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Requesty API-Schlüssel",
"refreshModels": {
"label": "Modelle aktualisieren",
"noModelsFound": "Keine Modelle gefunden. Bitte versuche es erneut.",
"hint": "Bitte öffne die Einstellungen erneut, um die neuesten Modelle zu sehen.",
"loading": "Modellliste wird aktualisiert...",
"success": "Modellliste erfolgreich aktualisiert!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Requesty API Key",
"refreshModels": {
"label": "Refresh Models",
"noModelsFound": "No models found. Please try again.",
"hint": "Please reopen the settings to see the latest models.",
"loading": "Refreshing models list...",
"success": "Models list refreshed successfully!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Clave API de Requesty",
"refreshModels": {
"label": "Actualizar modelos",
"noModelsFound": "No se encontraron modelos. Por favor, inténtalo de nuevo.",
"hint": "Por favor, vuelve a abrir la configuración para ver los modelos más recientes.",
"loading": "Actualizando lista de modelos...",
"success": "¡Lista de modelos actualizada correctamente!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Clé API Requesty",
"refreshModels": {
"label": "Actualiser les modèles",
"noModelsFound": "Aucun modèle trouvé. Veuillez réessayer.",
"hint": "Veuillez rouvrir les paramètres pour voir les modèles les plus récents.",
"loading": "Actualisation de la liste des modèles...",
"success": "Liste des modèles actualisée avec succès !",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Requesty API कुंजी",
"refreshModels": {
"label": "मॉडल रिफ्रेश करें",
"noModelsFound": "कोई मॉडल नहीं मिला। कृपया फिर से कोशिश करें।",
"hint": "नवीनतम मॉडल देखने के लिए कृपया सेटिंग्स को फिर से खोलें।",
"loading": "मॉडल सूची अपडेट हो रही है...",
"success": "मॉडल सूची सफलतापूर्वक अपडेट की गई!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Chiave API Requesty",
"refreshModels": {
"label": "Aggiorna modelli",
"noModelsFound": "Nessun modello trovato. Riprova.",
"hint": "Riapri le impostazioni per vedere i modelli più recenti.",
"loading": "Aggiornamento dell'elenco dei modelli...",
"success": "Elenco dei modelli aggiornato con successo!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Requesty APIキー",
"refreshModels": {
"label": "モデルを更新",
"noModelsFound": "モデルが見つかりません。もう一度お試しください。",
"hint": "最新のモデルを表示するには設定を再度開いてください。",
"loading": "モデルリストを更新中...",
"success": "モデルリストが正常に更新されました!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Requesty API 키",
"refreshModels": {
"label": "모델 새로고침",
"noModelsFound": "모델을 찾을 수 없습니다. 다시 시도해주세요.",
"hint": "최신 모델을 보려면 설정을 다시 열어주세요.",
"loading": "모델 목록 새로고침 중...",
"success": "모델 목록이 성공적으로 새로고침되었습니다!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Requesty API-sleutel",
"refreshModels": {
"label": "Modellen verversen",
"noModelsFound": "Geen modellen gevonden. Probeer het opnieuw.",
"hint": "Open de instellingen opnieuw om de nieuwste modellen te zien.",
"loading": "Modellenlijst wordt vernieuwd...",
"success": "Modellenlijst succesvol vernieuwd!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Klucz API Requesty",
"refreshModels": {
"label": "Odśwież modele",
"noModelsFound": "Nie znaleziono modeli. Spróbuj ponownie.",
"hint": "Proszę ponownie otworzyć ustawienia, aby zobaczyć najnowsze modele.",
"loading": "Odświeżanie listy modeli...",
"success": "Lista modeli została pomyślnie odświeżona!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Chave de API Requesty",
"refreshModels": {
"label": "Atualizar modelos",
"noModelsFound": "Nenhum modelo encontrado. Tente novamente.",
"hint": "Por favor, reabra as configurações para ver os modelos mais recentes.",
"loading": "Atualizando lista de modelos...",
"success": "Lista de modelos atualizada com sucesso!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Requesty API-ключ",
"refreshModels": {
"label": "Обновить модели",
"noModelsFound": "Модели не найдены. Попробуйте еще раз.",
"hint": "Пожалуйста, откройте настройки заново, чтобы увидеть последние модели.",
"loading": "Обновление списка моделей...",
"success": "Список моделей успешно обновлен!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Requesty API Anahtarı",
"refreshModels": {
"label": "Modelleri Yenile",
"noModelsFound": "Model bulunamadı. Lütfen tekrar deneyin.",
"hint": "En son modelleri görmek için lütfen ayarları yeniden açın.",
"loading": "Model listesi yenileniyor...",
"success": "Model listesi başarıyla yenilendi!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Khóa API Requesty",
"refreshModels": {
"label": "Làm mới mô hình",
"noModelsFound": "Không tìm thấy mô hình nào. Vui lòng thử lại.",
"hint": "Vui lòng mở lại cài đặt để xem các mô hình mới nhất.",
"loading": "Đang làm mới danh sách mô hình...",
"success": "Danh sách mô hình đã được làm mới thành công!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Requesty API 密钥",
"refreshModels": {
"label": "刷新模型",
"noModelsFound": "未找到模型。请重试。",
"hint": "请重新打开设置以查看最新模型。",
"loading": "正在刷新模型列表...",
"success": "模型列表刷新成功!",

View file

@ -152,6 +152,7 @@
"requestyApiKey": "Requesty API 金鑰",
"refreshModels": {
"label": "重新整理模型",
"noModelsFound": "找不到模型。請重試。",
"hint": "請重新開啟設定以查看最新模型。",
"loading": "正在重新整理模型列表...",
"success": "模型列表重新整理成功!",