mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-09 22:31:08 +00:00
litellm working with single provider models query
This commit is contained in:
parent
6762b579b2
commit
b6a2d5827e
45 changed files with 964 additions and 736 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
100
src/api/providers/fetchers/index.ts
Normal file
100
src/api/providers/fetchers/index.ts
Normal 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,
|
||||
}
|
||||
|
|
@ -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.`,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!)
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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", "")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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/"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
180
webview-ui/src/components/ui/hooks/useProviderModels.ts
Normal file
180
webview-ui/src/components/ui/hooks/useProviderModels.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
|
|
@ -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] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
|
|||
|
|
@ -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 !",
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@
|
|||
"requestyApiKey": "Requesty API कुंजी",
|
||||
"refreshModels": {
|
||||
"label": "मॉडल रिफ्रेश करें",
|
||||
"noModelsFound": "कोई मॉडल नहीं मिला। कृपया फिर से कोशिश करें।",
|
||||
"hint": "नवीनतम मॉडल देखने के लिए कृपया सेटिंग्स को फिर से खोलें।",
|
||||
"loading": "मॉडल सूची अपडेट हो रही है...",
|
||||
"success": "मॉडल सूची सफलतापूर्वक अपडेट की गई!",
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@
|
|||
"requestyApiKey": "Requesty APIキー",
|
||||
"refreshModels": {
|
||||
"label": "モデルを更新",
|
||||
"noModelsFound": "モデルが見つかりません。もう一度お試しください。",
|
||||
"hint": "最新のモデルを表示するには設定を再度開いてください。",
|
||||
"loading": "モデルリストを更新中...",
|
||||
"success": "モデルリストが正常に更新されました!",
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@
|
|||
"requestyApiKey": "Requesty API 키",
|
||||
"refreshModels": {
|
||||
"label": "모델 새로고침",
|
||||
"noModelsFound": "모델을 찾을 수 없습니다. 다시 시도해주세요.",
|
||||
"hint": "최신 모델을 보려면 설정을 다시 열어주세요.",
|
||||
"loading": "모델 목록 새로고침 중...",
|
||||
"success": "모델 목록이 성공적으로 새로고침되었습니다!",
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@
|
|||
"requestyApiKey": "Requesty API-ключ",
|
||||
"refreshModels": {
|
||||
"label": "Обновить модели",
|
||||
"noModelsFound": "Модели не найдены. Попробуйте еще раз.",
|
||||
"hint": "Пожалуйста, откройте настройки заново, чтобы увидеть последние модели.",
|
||||
"loading": "Обновление списка моделей...",
|
||||
"success": "Список моделей успешно обновлен!",
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@
|
|||
"requestyApiKey": "Requesty API 密钥",
|
||||
"refreshModels": {
|
||||
"label": "刷新模型",
|
||||
"noModelsFound": "未找到模型。请重试。",
|
||||
"hint": "请重新打开设置以查看最新模型。",
|
||||
"loading": "正在刷新模型列表...",
|
||||
"success": "模型列表刷新成功!",
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@
|
|||
"requestyApiKey": "Requesty API 金鑰",
|
||||
"refreshModels": {
|
||||
"label": "重新整理模型",
|
||||
"noModelsFound": "找不到模型。請重試。",
|
||||
"hint": "請重新開啟設定以查看最新模型。",
|
||||
"loading": "正在重新整理模型列表...",
|
||||
"success": "模型列表重新整理成功!",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue