mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Propagate AbortSignal to all provider fetchers and use native timeout
- Remove inline withTimeout helper in favor of AbortSignal.timeout() - Add optional AbortSignal parameter to all provider model fetchers: - openrouter, requesty, glama, unbound, litellm, ollama, lmstudio - deepinfra, io-intelligence, vercel-ai-gateway, huggingface, roo - Standardize timeout handling across modelCache and modelEndpointCache - Add useRouterModelsAll hook for settings UI to fetch all providers - Update Unbound and ApiOptions to use requestRouterModelsAll This ensures consistent cancellation behavior and prepares for better request lifecycle management across the codebase.
This commit is contained in:
parent
632bbe77db
commit
c4ddbf65d0
18 changed files with 375 additions and 350 deletions
|
|
@ -35,6 +35,7 @@ const DeepInfraModelsResponseSchema = z.object({ data: z.array(DeepInfraModelSch
|
|||
export async function getDeepInfraModels(
|
||||
apiKey?: string,
|
||||
baseUrl: string = "https://api.deepinfra.com/v1/openai",
|
||||
signal?: AbortSignal,
|
||||
): Promise<Record<string, ModelInfo>> {
|
||||
const headers: Record<string, string> = { ...DEFAULT_HEADERS }
|
||||
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`
|
||||
|
|
@ -42,7 +43,7 @@ export async function getDeepInfraModels(
|
|||
const url = `${baseUrl.replace(/\/$/, "")}/models`
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
|
||||
const response = await axios.get(url, { headers })
|
||||
const response = await axios.get(url, { headers, signal })
|
||||
const parsed = DeepInfraModelsResponseSchema.safeParse(response.data)
|
||||
const data = parsed.success ? parsed.data.data : response.data?.data || []
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import type { ModelInfo } from "@roo-code/types"
|
|||
|
||||
import { parseApiPrice } from "../../../shared/cost"
|
||||
|
||||
export async function getGlamaModels(): Promise<Record<string, ModelInfo>> {
|
||||
export async function getGlamaModels(signal?: AbortSignal): Promise<Record<string, ModelInfo>> {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
|
||||
try {
|
||||
const response = await axios.get("https://glama.ai/api/gateway/v1/models")
|
||||
const response = await axios.get("https://glama.ai/api/gateway/v1/models", { signal })
|
||||
const rawModels = response.data
|
||||
|
||||
for (const rawModel of rawModels) {
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ function parseHuggingFaceModel(model: HuggingFaceModel, provider?: HuggingFacePr
|
|||
* @returns A promise that resolves to a record of model IDs to model info
|
||||
* @throws Will throw an error if the request fails
|
||||
*/
|
||||
export async function getHuggingFaceModels(): Promise<ModelRecord> {
|
||||
export async function getHuggingFaceModels(signal?: AbortSignal): Promise<ModelRecord> {
|
||||
const now = Date.now()
|
||||
|
||||
if (cache && now - cache.timestamp < HUGGINGFACE_CACHE_DURATION) {
|
||||
|
|
@ -128,7 +128,7 @@ export async function getHuggingFaceModels(): Promise<ModelRecord> {
|
|||
Pragma: "no-cache",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
timeout: 10000,
|
||||
signal,
|
||||
})
|
||||
|
||||
const result = huggingFaceApiResponseSchema.safeParse(response.data)
|
||||
|
|
@ -236,7 +236,7 @@ export async function getHuggingFaceModelsWithMetadata(): Promise<HuggingFaceMod
|
|||
Pragma: "no-cache",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
timeout: 10000,
|
||||
signal: AbortSignal.timeout(30000),
|
||||
})
|
||||
|
||||
const models = response.data?.data || []
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ function parseIOIntelligenceModel(model: IOIntelligenceModel): ModelInfo {
|
|||
* Fetches available models from IO Intelligence
|
||||
* <mcreference link="https://docs.io.net/reference/get-started-with-io-intelligence-api" index="1">1</mcreference>
|
||||
*/
|
||||
export async function getIOIntelligenceModels(apiKey?: string): Promise<ModelRecord> {
|
||||
export async function getIOIntelligenceModels(apiKey?: string, signal?: AbortSignal): Promise<ModelRecord> {
|
||||
const now = Date.now()
|
||||
|
||||
if (cache && now - cache.timestamp < IO_INTELLIGENCE_CACHE_DURATION) {
|
||||
|
|
@ -108,7 +108,7 @@ export async function getIOIntelligenceModels(apiKey?: string): Promise<ModelRec
|
|||
"https://api.intelligence.io.solutions/api/v1/models",
|
||||
{
|
||||
headers,
|
||||
timeout: 10_000,
|
||||
signal,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { DEFAULT_HEADERS } from "../constants"
|
|||
* @returns A promise that resolves to a record of model IDs to model info
|
||||
* @throws Will throw an error if the request fails or the response is not as expected.
|
||||
*/
|
||||
export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise<ModelRecord> {
|
||||
export async function getLiteLLMModels(apiKey: string, baseUrl: string, signal?: AbortSignal): Promise<ModelRecord> {
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -27,8 +27,7 @@ export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise
|
|||
// Normalize the pathname by removing trailing slashes and multiple slashes
|
||||
urlObj.pathname = urlObj.pathname.replace(/\/+$/, "").replace(/\/+/g, "/") + "/v1/model/info"
|
||||
const url = urlObj.href
|
||||
// Added timeout to prevent indefinite hanging
|
||||
const response = await axios.get(url, { headers, timeout: 5000 })
|
||||
const response = await axios.get(url, { headers, signal })
|
||||
const models: ModelRecord = {}
|
||||
|
||||
// Process the model info from the response
|
||||
|
|
|
|||
|
|
@ -49,7 +49,10 @@ export const parseLMStudioModel = (rawModel: LLMInstanceInfo | LLMInfo): ModelIn
|
|||
return modelInfo
|
||||
}
|
||||
|
||||
export async function getLMStudioModels(baseUrl = "http://localhost:1234"): Promise<Record<string, ModelInfo>> {
|
||||
export async function getLMStudioModels(
|
||||
baseUrl = "http://localhost:1234",
|
||||
signal?: AbortSignal,
|
||||
): Promise<Record<string, ModelInfo>> {
|
||||
// clear the set of models that have full details loaded
|
||||
modelsWithLoadedDetails.clear()
|
||||
// clearing the input can leave an empty string; use the default in that case
|
||||
|
|
@ -66,7 +69,7 @@ export async function getLMStudioModels(baseUrl = "http://localhost:1234"): Prom
|
|||
|
||||
// test the connection to LM Studio first
|
||||
// errors will be caught further down
|
||||
await axios.get(`${baseUrl}/v1/models`)
|
||||
await axios.get(`${baseUrl}/v1/models`, { signal })
|
||||
|
||||
const client = new LMStudioClient({ baseUrl: lmsUrl })
|
||||
|
||||
|
|
|
|||
|
|
@ -31,19 +31,6 @@ const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
|
|||
// Coalesce concurrent fetches per provider within this extension host
|
||||
const inFlightModelFetches = new Map<RouterName, Promise<ModelRecord>>()
|
||||
|
||||
function withTimeout<T>(p: Promise<T>, ms: number, label = "getModels"): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const t = setTimeout(() => reject(new Error(`${label} timeout after ${ms}ms`)), ms)
|
||||
p.then((v) => {
|
||||
clearTimeout(t)
|
||||
resolve(v)
|
||||
}).catch((e) => {
|
||||
clearTimeout(t)
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function writeModels(router: RouterName, data: ModelRecord) {
|
||||
const filename = `${router}_models.json`
|
||||
const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath)
|
||||
|
|
@ -76,6 +63,7 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
|
|||
// 1) Try memory cache
|
||||
const cached = getModelsFromCache(provider)
|
||||
if (cached) {
|
||||
// Using console.log for cache layer logging (no provider access in utility functions)
|
||||
console.log(`[modelCache] cache_hit: ${providerStr} (${Object.keys(cached).length} models)`)
|
||||
return cached
|
||||
}
|
||||
|
|
@ -84,78 +72,83 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
|
|||
try {
|
||||
const file = await readModels(provider)
|
||||
if (file && Object.keys(file).length > 0) {
|
||||
// Using console.log for cache layer logging (no provider access in utility functions)
|
||||
console.log(`[modelCache] file_hit: ${providerStr} (${Object.keys(file).length} models, bg_refresh queued)`)
|
||||
// Populate memory cache immediately so follow-up callers are instant
|
||||
memoryCache.set(provider, file)
|
||||
|
||||
// Start background refresh if not already in-flight (do not await)
|
||||
if (!inFlightModelFetches.has(provider)) {
|
||||
const signal = AbortSignal.timeout(30_000)
|
||||
const bgPromise = (async (): Promise<ModelRecord> => {
|
||||
let models: ModelRecord = {}
|
||||
try {
|
||||
switch (providerStr) {
|
||||
case "openrouter":
|
||||
models = await getOpenRouterModels()
|
||||
break
|
||||
case "requesty":
|
||||
models = await getRequestyModels(options.baseUrl, options.apiKey)
|
||||
break
|
||||
case "glama":
|
||||
models = await getGlamaModels()
|
||||
break
|
||||
case "unbound":
|
||||
models = await getUnboundModels(options.apiKey)
|
||||
break
|
||||
case "litellm":
|
||||
models = await getLiteLLMModels(options.apiKey as string, options.baseUrl as string)
|
||||
break
|
||||
case "ollama":
|
||||
models = await getOllamaModels(options.baseUrl, options.apiKey)
|
||||
break
|
||||
case "lmstudio":
|
||||
models = await getLMStudioModels(options.baseUrl)
|
||||
break
|
||||
case "deepinfra":
|
||||
models = await getDeepInfraModels(options.apiKey, options.baseUrl)
|
||||
break
|
||||
case "io-intelligence":
|
||||
models = await getIOIntelligenceModels(options.apiKey)
|
||||
break
|
||||
case "vercel-ai-gateway":
|
||||
models = await getVercelAiGatewayModels()
|
||||
break
|
||||
case "huggingface":
|
||||
models = await getHuggingFaceModels()
|
||||
break
|
||||
case "roo": {
|
||||
const rooBaseUrl =
|
||||
options.baseUrl ??
|
||||
process.env.ROO_CODE_PROVIDER_URL ??
|
||||
"https://api.roocode.com/proxy"
|
||||
models = await getRooModels(rooBaseUrl, options.apiKey)
|
||||
break
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown provider: ${providerStr}`)
|
||||
switch (providerStr) {
|
||||
case "openrouter":
|
||||
models = await getOpenRouterModels(undefined, signal)
|
||||
break
|
||||
case "requesty":
|
||||
models = await getRequestyModels(options.baseUrl, options.apiKey, signal)
|
||||
break
|
||||
case "glama":
|
||||
models = await getGlamaModels(signal)
|
||||
break
|
||||
case "unbound":
|
||||
models = await getUnboundModels(options.apiKey, signal)
|
||||
break
|
||||
case "litellm":
|
||||
models = await getLiteLLMModels(options.apiKey as string, options.baseUrl as string, signal)
|
||||
break
|
||||
case "ollama":
|
||||
models = await getOllamaModels(options.baseUrl, options.apiKey, signal)
|
||||
break
|
||||
case "lmstudio":
|
||||
models = await getLMStudioModels(options.baseUrl, signal)
|
||||
break
|
||||
case "deepinfra":
|
||||
models = await getDeepInfraModels(options.apiKey, options.baseUrl, signal)
|
||||
break
|
||||
case "io-intelligence":
|
||||
models = await getIOIntelligenceModels(options.apiKey, signal)
|
||||
break
|
||||
case "vercel-ai-gateway":
|
||||
models = await getVercelAiGatewayModels(undefined, signal)
|
||||
break
|
||||
case "huggingface":
|
||||
models = await getHuggingFaceModels(signal)
|
||||
break
|
||||
case "roo": {
|
||||
const rooBaseUrl =
|
||||
options.baseUrl ?? process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy"
|
||||
models = await getRooModels(rooBaseUrl, options.apiKey, signal)
|
||||
break
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[modelCache] bg_refresh_done: ${providerStr} (${Object.keys(models || {}).length} models)`,
|
||||
)
|
||||
memoryCache.set(provider, models)
|
||||
await writeModels(provider, models).catch((err) =>
|
||||
console.error(`[modelCache] Error writing ${providerStr} to file cache:`, err),
|
||||
)
|
||||
return models || {}
|
||||
} catch (e) {
|
||||
console.error(`[modelCache] bg_refresh_failed: ${providerStr}`, e)
|
||||
throw e
|
||||
default:
|
||||
throw new Error(`Unknown provider: ${providerStr}`)
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[modelCache] bg_refresh_done: ${providerStr} (${Object.keys(models || {}).length} models)`,
|
||||
)
|
||||
memoryCache.set(provider, models)
|
||||
await writeModels(provider, models).catch((err) => {
|
||||
console.error(
|
||||
`[modelCache] Error writing ${providerStr} to file cache during background refresh:`,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
)
|
||||
})
|
||||
return models || {}
|
||||
})()
|
||||
|
||||
const timedBg = withTimeout(bgPromise, 30_000, `getModels(background:${providerStr})`)
|
||||
inFlightModelFetches.set(provider, timedBg)
|
||||
Promise.resolve(timedBg).finally(() => inFlightModelFetches.delete(provider))
|
||||
inFlightModelFetches.set(provider, bgPromise)
|
||||
Promise.resolve(bgPromise)
|
||||
.catch((err) => {
|
||||
// Log background refresh failures for monitoring
|
||||
console.error(
|
||||
`[modelCache] Background refresh failed for ${providerStr}:`,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
)
|
||||
})
|
||||
.finally(() => inFlightModelFetches.delete(provider))
|
||||
}
|
||||
|
||||
// Return the file snapshot immediately
|
||||
|
|
@ -168,82 +161,81 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
|
|||
// 3) Coalesce concurrent fetches
|
||||
const existing = inFlightModelFetches.get(provider)
|
||||
if (existing) {
|
||||
// Using console.log for cache layer logging (no provider access in utility functions)
|
||||
console.log(`[modelCache] coalesced_wait: ${providerStr}`)
|
||||
return existing
|
||||
}
|
||||
|
||||
// 4) Network fetch wrapped as a single in-flight promise for this provider
|
||||
const signal = AbortSignal.timeout(30_000)
|
||||
const fetchPromise = (async (): Promise<ModelRecord> => {
|
||||
let models: ModelRecord = {}
|
||||
try {
|
||||
switch (providerStr) {
|
||||
case "openrouter":
|
||||
models = await getOpenRouterModels()
|
||||
break
|
||||
case "requesty":
|
||||
models = await getRequestyModels(options.baseUrl, options.apiKey)
|
||||
break
|
||||
case "glama":
|
||||
models = await getGlamaModels()
|
||||
break
|
||||
case "unbound":
|
||||
models = await getUnboundModels(options.apiKey)
|
||||
break
|
||||
case "litellm":
|
||||
models = await getLiteLLMModels(options.apiKey as string, options.baseUrl as string)
|
||||
break
|
||||
case "ollama":
|
||||
models = await getOllamaModels(options.baseUrl, options.apiKey)
|
||||
break
|
||||
case "lmstudio":
|
||||
models = await getLMStudioModels(options.baseUrl)
|
||||
break
|
||||
case "deepinfra":
|
||||
models = await getDeepInfraModels(options.apiKey, options.baseUrl)
|
||||
break
|
||||
case "io-intelligence":
|
||||
models = await getIOIntelligenceModels(options.apiKey)
|
||||
break
|
||||
case "vercel-ai-gateway":
|
||||
models = await getVercelAiGatewayModels()
|
||||
break
|
||||
case "huggingface":
|
||||
models = await getHuggingFaceModels()
|
||||
break
|
||||
case "roo": {
|
||||
const rooBaseUrl =
|
||||
options.baseUrl ?? process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy"
|
||||
models = await getRooModels(rooBaseUrl, options.apiKey)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown provider: ${providerStr}`)
|
||||
}
|
||||
switch (providerStr) {
|
||||
case "openrouter":
|
||||
models = await getOpenRouterModels(undefined, signal)
|
||||
break
|
||||
case "requesty":
|
||||
models = await getRequestyModels(options.baseUrl, options.apiKey, signal)
|
||||
break
|
||||
case "glama":
|
||||
models = await getGlamaModels(signal)
|
||||
break
|
||||
case "unbound":
|
||||
models = await getUnboundModels(options.apiKey, signal)
|
||||
break
|
||||
case "litellm":
|
||||
models = await getLiteLLMModels(options.apiKey as string, options.baseUrl as string, signal)
|
||||
break
|
||||
case "ollama":
|
||||
models = await getOllamaModels(options.baseUrl, options.apiKey, signal)
|
||||
break
|
||||
case "lmstudio":
|
||||
models = await getLMStudioModels(options.baseUrl, signal)
|
||||
break
|
||||
case "deepinfra":
|
||||
models = await getDeepInfraModels(options.apiKey, options.baseUrl, signal)
|
||||
break
|
||||
case "io-intelligence":
|
||||
models = await getIOIntelligenceModels(options.apiKey, signal)
|
||||
break
|
||||
case "vercel-ai-gateway":
|
||||
models = await getVercelAiGatewayModels(undefined, signal)
|
||||
break
|
||||
case "huggingface":
|
||||
models = await getHuggingFaceModels(signal)
|
||||
break
|
||||
case "roo": {
|
||||
const rooBaseUrl =
|
||||
options.baseUrl ?? process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy"
|
||||
models = await getRooModels(rooBaseUrl, options.apiKey, signal)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown provider: ${providerStr}`)
|
||||
}
|
||||
|
||||
console.log(`[modelCache] network_fetch_done: ${providerStr} (${Object.keys(models || {}).length} models)`)
|
||||
|
||||
// Update memory cache first so waiters get immediate hits
|
||||
memoryCache.set(provider, models)
|
||||
|
||||
// Persist to file cache (best-effort)
|
||||
await writeModels(provider, models).catch((err) =>
|
||||
console.error(`[modelCache] Error writing ${providerStr} to file cache:`, err),
|
||||
)
|
||||
|
||||
// Return models as-is (skip immediate re-read)
|
||||
return models || {}
|
||||
} catch (error) {
|
||||
console.error(`[modelCache] network_fetch_failed: ${providerStr}`, error)
|
||||
throw error
|
||||
}
|
||||
|
||||
console.log(`[modelCache] network_fetch_done: ${providerStr} (${Object.keys(models || {}).length} models)`)
|
||||
|
||||
// Update memory cache first so waiters get immediate hits
|
||||
memoryCache.set(provider, models)
|
||||
|
||||
// Persist to file cache (best-effort)
|
||||
await writeModels(provider, models).catch((err) => {
|
||||
console.error(
|
||||
`[modelCache] Error writing ${providerStr} to file cache after network fetch:`,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
)
|
||||
})
|
||||
|
||||
// Return models as-is (skip immediate re-read)
|
||||
return models || {}
|
||||
})()
|
||||
|
||||
// Register and await with timeout; ensure cleanup
|
||||
const timed = withTimeout(fetchPromise, 30_000, `getModels(${providerStr})`)
|
||||
inFlightModelFetches.set(provider, timed)
|
||||
// Register and await; ensure cleanup
|
||||
inFlightModelFetches.set(provider, fetchPromise)
|
||||
try {
|
||||
return await timed
|
||||
return await fetchPromise
|
||||
} finally {
|
||||
inFlightModelFetches.delete(provider)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,19 +17,6 @@ const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
|
|||
// Coalesce concurrent endpoint fetches per (router,modelId)
|
||||
const inFlightEndpointFetches = new Map<string, Promise<ModelRecord>>()
|
||||
|
||||
function withTimeout<T>(p: Promise<T>, ms: number, label = "getModelEndpoints"): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const t = setTimeout(() => reject(new Error(`${label} timeout after ${ms}ms`)), ms)
|
||||
p.then((v) => {
|
||||
clearTimeout(t)
|
||||
resolve(v)
|
||||
}).catch((e) => {
|
||||
clearTimeout(t)
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const getCacheKey = (router: RouterName, modelId: string) => sanitize(`${router}_${modelId}`)
|
||||
|
||||
async function writeModelEndpoints(key: string, data: ModelRecord) {
|
||||
|
|
@ -66,6 +53,7 @@ export const getModelEndpoints = async ({
|
|||
// 1) Try memory cache
|
||||
const cached = memoryCache.get<ModelRecord>(key)
|
||||
if (cached) {
|
||||
// Using console.log for cache layer logging (no provider access in utility functions)
|
||||
console.log(`[endpointCache] cache_hit: ${key} (${Object.keys(cached).length} endpoints)`)
|
||||
return cached
|
||||
}
|
||||
|
|
@ -74,37 +62,44 @@ export const getModelEndpoints = async ({
|
|||
try {
|
||||
const file = await readModelEndpoints(key)
|
||||
if (file && Object.keys(file).length > 0) {
|
||||
// Using console.log for cache layer logging (no provider access in utility functions)
|
||||
console.log(`[endpointCache] file_hit: ${key} (${Object.keys(file).length} endpoints, bg_refresh queued)`)
|
||||
// Populate memory cache immediately
|
||||
memoryCache.set(key, file)
|
||||
|
||||
// Start background refresh if not already in-flight (do not await)
|
||||
if (!inFlightEndpointFetches.has(key)) {
|
||||
const signal = AbortSignal.timeout(30_000)
|
||||
const bgPromise = (async (): Promise<ModelRecord> => {
|
||||
try {
|
||||
const modelProviders = await getOpenRouterModelEndpoints(modelId)
|
||||
if (Object.keys(modelProviders).length > 0) {
|
||||
console.log(
|
||||
`[endpointCache] bg_refresh_done: ${key} (${Object.keys(modelProviders).length} endpoints)`,
|
||||
const modelProviders = await getOpenRouterModelEndpoints(modelId, undefined, signal)
|
||||
if (Object.keys(modelProviders).length > 0) {
|
||||
console.log(
|
||||
`[endpointCache] bg_refresh_done: ${key} (${Object.keys(modelProviders).length} endpoints)`,
|
||||
)
|
||||
memoryCache.set(key, modelProviders)
|
||||
try {
|
||||
await writeModelEndpoints(key, modelProviders)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[endpointCache] Error writing ${key} to file cache during background refresh:`,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
)
|
||||
memoryCache.set(key, modelProviders)
|
||||
try {
|
||||
await writeModelEndpoints(key, modelProviders)
|
||||
} catch (error) {
|
||||
console.error(`[endpointCache] Error writing ${key} to file cache`, error)
|
||||
}
|
||||
return modelProviders
|
||||
}
|
||||
return {}
|
||||
} catch (e) {
|
||||
console.error(`[endpointCache] bg_refresh_failed: ${key}`, e)
|
||||
throw e
|
||||
return modelProviders
|
||||
}
|
||||
return {}
|
||||
})()
|
||||
|
||||
const timedBg = withTimeout(bgPromise, 30_000, `getModelEndpoints(background:${key})`)
|
||||
inFlightEndpointFetches.set(key, timedBg)
|
||||
Promise.resolve(timedBg).finally(() => inFlightEndpointFetches.delete(key))
|
||||
inFlightEndpointFetches.set(key, bgPromise)
|
||||
Promise.resolve(bgPromise)
|
||||
.catch((err) => {
|
||||
// Log background refresh failures for monitoring
|
||||
console.error(
|
||||
`[endpointCache] Background refresh failed for ${key}:`,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
)
|
||||
})
|
||||
.finally(() => inFlightEndpointFetches.delete(key))
|
||||
}
|
||||
|
||||
return file
|
||||
|
|
@ -116,50 +111,47 @@ export const getModelEndpoints = async ({
|
|||
// 3) Coalesce concurrent fetches
|
||||
const inFlight = inFlightEndpointFetches.get(key)
|
||||
if (inFlight) {
|
||||
// Using console.log for cache layer logging (no provider access in utility functions)
|
||||
console.log(`[endpointCache] coalesced_wait: ${key}`)
|
||||
return inFlight
|
||||
}
|
||||
|
||||
// 4) Single network fetch for this key
|
||||
const signal = AbortSignal.timeout(30_000)
|
||||
const fetchPromise = (async (): Promise<ModelRecord> => {
|
||||
let modelProviders: ModelRecord = {}
|
||||
try {
|
||||
modelProviders = await getOpenRouterModelEndpoints(modelId)
|
||||
modelProviders = await getOpenRouterModelEndpoints(modelId, undefined, signal)
|
||||
|
||||
if (Object.keys(modelProviders).length > 0) {
|
||||
console.log(
|
||||
`[endpointCache] network_fetch_done: ${key} (${Object.keys(modelProviders).length} endpoints)`,
|
||||
)
|
||||
// Update memory cache first
|
||||
memoryCache.set(key, modelProviders)
|
||||
if (Object.keys(modelProviders).length > 0) {
|
||||
console.log(`[endpointCache] network_fetch_done: ${key} (${Object.keys(modelProviders).length} endpoints)`)
|
||||
// Update memory cache first
|
||||
memoryCache.set(key, modelProviders)
|
||||
|
||||
// Best-effort persist
|
||||
try {
|
||||
await writeModelEndpoints(key, modelProviders)
|
||||
} catch (error) {
|
||||
console.error(`[endpointCache] Error writing ${key} to file cache`, error)
|
||||
}
|
||||
|
||||
return modelProviders
|
||||
}
|
||||
|
||||
// Fallback to file cache if network returned empty (rare)
|
||||
// Best-effort persist
|
||||
try {
|
||||
const file = await readModelEndpoints(key)
|
||||
return file ?? {}
|
||||
} catch {
|
||||
return {}
|
||||
await writeModelEndpoints(key, modelProviders)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[endpointCache] Error writing ${key} to file cache after network fetch:`,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[endpointCache] network_fetch_failed: ${key}`, error)
|
||||
throw error
|
||||
|
||||
return modelProviders
|
||||
}
|
||||
|
||||
// Fallback to file cache if network returned empty (rare)
|
||||
try {
|
||||
const file = await readModelEndpoints(key)
|
||||
return file ?? {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
})()
|
||||
|
||||
const timed = withTimeout(fetchPromise, 30_000, `getModelEndpoints(${key})`)
|
||||
inFlightEndpointFetches.set(key, timed)
|
||||
inFlightEndpointFetches.set(key, fetchPromise)
|
||||
try {
|
||||
return await timed
|
||||
return await fetchPromise
|
||||
} finally {
|
||||
inFlightEndpointFetches.delete(key)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo =
|
|||
export async function getOllamaModels(
|
||||
baseUrl = "http://localhost:11434",
|
||||
apiKey?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Record<string, ModelInfo>> {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
|
||||
|
|
@ -73,7 +74,7 @@ export async function getOllamaModels(
|
|||
headers["Authorization"] = `Bearer ${apiKey}`
|
||||
}
|
||||
|
||||
const response = await axios.get<OllamaModelsResponse>(`${baseUrl}/api/tags`, { headers })
|
||||
const response = await axios.get<OllamaModelsResponse>(`${baseUrl}/api/tags`, { headers, signal })
|
||||
const parsedResponse = OllamaModelsResponseSchema.safeParse(response.data)
|
||||
let modelInfoPromises = []
|
||||
|
||||
|
|
@ -86,7 +87,7 @@ export async function getOllamaModels(
|
|||
{
|
||||
model: ollamaModel.model,
|
||||
},
|
||||
{ headers },
|
||||
{ headers, signal },
|
||||
)
|
||||
.then((ollamaModelInfo) => {
|
||||
models[ollamaModel.name] = parseOllamaModel(ollamaModelInfo.data)
|
||||
|
|
|
|||
|
|
@ -94,12 +94,15 @@ type OpenRouterModelEndpointsResponse = z.infer<typeof openRouterModelEndpointsR
|
|||
* getOpenRouterModels
|
||||
*/
|
||||
|
||||
export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<Record<string, ModelInfo>> {
|
||||
export async function getOpenRouterModels(
|
||||
options?: ApiHandlerOptions,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Record<string, ModelInfo>> {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
const baseURL = options?.openRouterBaseUrl || "https://openrouter.ai/api/v1"
|
||||
|
||||
try {
|
||||
const response = await axios.get<OpenRouterModelsResponse>(`${baseURL}/models`)
|
||||
const response = await axios.get<OpenRouterModelsResponse>(`${baseURL}/models`, { signal })
|
||||
const result = openRouterModelsResponseSchema.safeParse(response.data)
|
||||
const data = result.success ? result.data.data : response.data.data
|
||||
|
||||
|
|
@ -140,12 +143,15 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<
|
|||
export async function getOpenRouterModelEndpoints(
|
||||
modelId: string,
|
||||
options?: ApiHandlerOptions,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Record<string, ModelInfo>> {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
const baseURL = options?.openRouterBaseUrl || "https://openrouter.ai/api/v1"
|
||||
|
||||
try {
|
||||
const response = await axios.get<OpenRouterModelEndpointsResponse>(`${baseURL}/models/${modelId}/endpoints`)
|
||||
const response = await axios.get<OpenRouterModelEndpointsResponse>(`${baseURL}/models/${modelId}/endpoints`, {
|
||||
signal,
|
||||
})
|
||||
const result = openRouterModelEndpointsResponseSchema.safeParse(response.data)
|
||||
const data = result.success ? result.data.data : response.data.data
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ import type { ModelInfo } from "@roo-code/types"
|
|||
import { parseApiPrice } from "../../../shared/cost"
|
||||
import { toRequestyServiceUrl } from "../../../shared/utils/requesty"
|
||||
|
||||
export async function getRequestyModels(baseUrl?: string, apiKey?: string): Promise<Record<string, ModelInfo>> {
|
||||
export async function getRequestyModels(
|
||||
baseUrl?: string,
|
||||
apiKey?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Record<string, ModelInfo>> {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
|
||||
try {
|
||||
|
|
@ -18,7 +22,7 @@ export async function getRequestyModels(baseUrl?: string, apiKey?: string): Prom
|
|||
const resolvedBaseUrl = toRequestyServiceUrl(baseUrl)
|
||||
const modelsUrl = new URL("v1/models", resolvedBaseUrl)
|
||||
|
||||
const response = await axios.get(modelsUrl.toString(), { headers })
|
||||
const response = await axios.get(modelsUrl.toString(), { headers, signal })
|
||||
const rawModels = response.data.data
|
||||
|
||||
for (const rawModel of rawModels) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { DEFAULT_HEADERS } from "../constants"
|
|||
* @returns A promise that resolves to a record of model IDs to model info
|
||||
* @throws Will throw an error if the request fails or the response is not as expected.
|
||||
*/
|
||||
export async function getRooModels(baseUrl: string, apiKey?: string): Promise<ModelRecord> {
|
||||
export async function getRooModels(baseUrl: string, apiKey?: string, signal?: AbortSignal): Promise<ModelRecord> {
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -29,87 +29,79 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise<Mo
|
|||
const normalizedBase = baseUrl.replace(/\/?v1\/?$/, "")
|
||||
const url = `${normalizedBase}/v1/models`
|
||||
|
||||
// Use fetch with AbortController for better timeout handling
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000)
|
||||
const response = await fetch(url, {
|
||||
headers,
|
||||
signal,
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const models: ModelRecord = {}
|
||||
|
||||
// Validate response against schema
|
||||
const parsed = RooModelsResponseSchema.safeParse(data)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("Error fetching Roo Code Cloud models: Unexpected response format", data)
|
||||
console.error("Validation errors:", parsed.error.format())
|
||||
throw new Error("Failed to fetch Roo Code Cloud models: Unexpected response format.")
|
||||
}
|
||||
|
||||
// Process the validated model data
|
||||
for (const model of parsed.data.data) {
|
||||
const modelId = model.id
|
||||
|
||||
if (!modelId) continue
|
||||
|
||||
// Extract model data from the validated API response
|
||||
// All required fields are guaranteed by the schema
|
||||
const contextWindow = model.context_window
|
||||
const maxTokens = model.max_tokens
|
||||
const tags = model.tags || []
|
||||
const pricing = model.pricing
|
||||
|
||||
// Determine if the model supports images based on tags
|
||||
const supportsImages = tags.includes("vision")
|
||||
|
||||
// Determine if the model supports reasoning effort based on tags
|
||||
const supportsReasoningEffort = tags.includes("reasoning")
|
||||
|
||||
// Determine if the model requires reasoning effort based on tags
|
||||
const requiredReasoningEffort = tags.includes("reasoning-required")
|
||||
|
||||
// Parse pricing (API returns strings, convert to numbers)
|
||||
const inputPrice = parseApiPrice(pricing.input)
|
||||
const outputPrice = parseApiPrice(pricing.output)
|
||||
const cacheReadPrice = pricing.input_cache_read ? parseApiPrice(pricing.input_cache_read) : undefined
|
||||
const cacheWritePrice = pricing.input_cache_write ? parseApiPrice(pricing.input_cache_write) : undefined
|
||||
|
||||
models[modelId] = {
|
||||
maxTokens,
|
||||
contextWindow,
|
||||
supportsImages,
|
||||
supportsReasoningEffort,
|
||||
requiredReasoningEffort,
|
||||
supportsPromptCache: Boolean(cacheReadPrice !== undefined),
|
||||
inputPrice,
|
||||
outputPrice,
|
||||
cacheWritesPrice: cacheWritePrice,
|
||||
cacheReadsPrice: cacheReadPrice,
|
||||
description: model.description || model.name,
|
||||
deprecated: model.deprecated || false,
|
||||
isFree: tags.includes("free"),
|
||||
}
|
||||
}
|
||||
|
||||
return models
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const models: ModelRecord = {}
|
||||
|
||||
// Validate response against schema
|
||||
const parsed = RooModelsResponseSchema.safeParse(data)
|
||||
|
||||
if (!parsed.success) {
|
||||
console.error("Error fetching Roo Code Cloud models: Unexpected response format", data)
|
||||
console.error("Validation errors:", parsed.error.format())
|
||||
throw new Error("Failed to fetch Roo Code Cloud models: Unexpected response format.")
|
||||
}
|
||||
|
||||
// Process the validated model data
|
||||
for (const model of parsed.data.data) {
|
||||
const modelId = model.id
|
||||
|
||||
if (!modelId) continue
|
||||
|
||||
// Extract model data from the validated API response
|
||||
// All required fields are guaranteed by the schema
|
||||
const contextWindow = model.context_window
|
||||
const maxTokens = model.max_tokens
|
||||
const tags = model.tags || []
|
||||
const pricing = model.pricing
|
||||
|
||||
// Determine if the model supports images based on tags
|
||||
const supportsImages = tags.includes("vision")
|
||||
|
||||
// Determine if the model supports reasoning effort based on tags
|
||||
const supportsReasoningEffort = tags.includes("reasoning")
|
||||
|
||||
// Determine if the model requires reasoning effort based on tags
|
||||
const requiredReasoningEffort = tags.includes("reasoning-required")
|
||||
|
||||
// Parse pricing (API returns strings, convert to numbers)
|
||||
const inputPrice = parseApiPrice(pricing.input)
|
||||
const outputPrice = parseApiPrice(pricing.output)
|
||||
const cacheReadPrice = pricing.input_cache_read ? parseApiPrice(pricing.input_cache_read) : undefined
|
||||
const cacheWritePrice = pricing.input_cache_write ? parseApiPrice(pricing.input_cache_write) : undefined
|
||||
|
||||
models[modelId] = {
|
||||
maxTokens,
|
||||
contextWindow,
|
||||
supportsImages,
|
||||
supportsReasoningEffort,
|
||||
requiredReasoningEffort,
|
||||
supportsPromptCache: Boolean(cacheReadPrice !== undefined),
|
||||
inputPrice,
|
||||
outputPrice,
|
||||
cacheWritesPrice: cacheWritePrice,
|
||||
cacheReadsPrice: cacheReadPrice,
|
||||
description: model.description || model.name,
|
||||
deprecated: model.deprecated || false,
|
||||
isFree: tags.includes("free"),
|
||||
}
|
||||
}
|
||||
|
||||
return models
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching Roo Code Cloud models:", error.message ? error.message : error)
|
||||
|
||||
// Handle abort/timeout
|
||||
if (error.name === "AbortError") {
|
||||
throw new Error("Failed to fetch Roo Code Cloud models: Request timed out after 10 seconds.")
|
||||
throw new Error("Failed to fetch Roo Code Cloud models: Request timed out.")
|
||||
}
|
||||
|
||||
// Handle fetch errors
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import axios from "axios"
|
|||
|
||||
import type { ModelInfo } from "@roo-code/types"
|
||||
|
||||
export async function getUnboundModels(apiKey?: string | null): Promise<Record<string, ModelInfo>> {
|
||||
export async function getUnboundModels(
|
||||
apiKey?: string | null,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Record<string, ModelInfo>> {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
|
||||
try {
|
||||
|
|
@ -12,7 +15,7 @@ export async function getUnboundModels(apiKey?: string | null): Promise<Record<s
|
|||
headers["Authorization"] = `Bearer ${apiKey}`
|
||||
}
|
||||
|
||||
const response = await axios.get("https://api.getunbound.ai/models", { headers })
|
||||
const response = await axios.get("https://api.getunbound.ai/models", { headers, signal })
|
||||
|
||||
if (response.data) {
|
||||
const rawModels: Record<string, any> = response.data
|
||||
|
|
|
|||
|
|
@ -52,12 +52,15 @@ type VercelAiGatewayModelsResponse = z.infer<typeof vercelAiGatewayModelsRespons
|
|||
* getVercelAiGatewayModels
|
||||
*/
|
||||
|
||||
export async function getVercelAiGatewayModels(options?: ApiHandlerOptions): Promise<Record<string, ModelInfo>> {
|
||||
export async function getVercelAiGatewayModels(
|
||||
options?: ApiHandlerOptions,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Record<string, ModelInfo>> {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
const baseURL = "https://ai-gateway.vercel.sh/v1"
|
||||
|
||||
try {
|
||||
const response = await axios.get<VercelAiGatewayModelsResponse>(`${baseURL}/models`)
|
||||
const response = await axios.get<VercelAiGatewayModelsResponse>(`${baseURL}/models`, { signal })
|
||||
const result = vercelAiGatewayModelsResponseSchema.safeParse(response.data)
|
||||
const data = result.success ? result.data.data : response.data.data
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import { ClineProvider } from "./ClineProvider"
|
|||
import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler"
|
||||
import { changeLanguage, t } from "../../i18n"
|
||||
import { Package } from "../../shared/package"
|
||||
import { type RouterName, type ModelRecord, isRouterName, toRouterName } from "../../shared/api"
|
||||
import { type RouterName, type ModelRecord, type RouterModels, isRouterName, toRouterName } from "../../shared/api"
|
||||
import { MessageEnhancer } from "./messageEnhancer"
|
||||
|
||||
import {
|
||||
|
|
@ -60,11 +60,6 @@ import { getCommand } from "../../utils/commands"
|
|||
|
||||
const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
|
||||
|
||||
// Phase 3: Debounce router model fetches to collapse rapid repeats
|
||||
const ROUTER_MODELS_DEBOUNCE_MS = process.env.NODE_ENV === "test" ? 0 : 400
|
||||
let lastRouterModelsRequestTime = 0
|
||||
let lastRouterModelsAllRequestTime = 0
|
||||
|
||||
import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace"
|
||||
import { setPendingTodoList } from "../tools/updateTodoListTool"
|
||||
|
||||
|
|
@ -771,21 +766,13 @@ export const webviewMessageHandler = async (
|
|||
await flushModels(routerNameFlush)
|
||||
break
|
||||
case "requestRouterModels": {
|
||||
// Phase 3: Debounce to collapse rapid repeats
|
||||
const now = Date.now()
|
||||
if (now - lastRouterModelsRequestTime < ROUTER_MODELS_DEBOUNCE_MS) {
|
||||
// Skip this request - too soon after last one
|
||||
break
|
||||
}
|
||||
lastRouterModelsRequestTime = now
|
||||
|
||||
// Phase 2: Scope to active provider during chat/task flows
|
||||
const { apiConfiguration } = await provider.getState()
|
||||
const providerStr = apiConfiguration.apiProvider
|
||||
const activeProvider: RouterName | undefined =
|
||||
providerStr && isRouterName(providerStr) ? providerStr : undefined
|
||||
|
||||
const routerModels: any = {
|
||||
const routerModels: Partial<Record<RouterName, ModelRecord>> = {
|
||||
openrouter: {},
|
||||
"vercel-ai-gateway": {},
|
||||
huggingface: {},
|
||||
|
|
@ -804,9 +791,8 @@ export const webviewMessageHandler = async (
|
|||
try {
|
||||
return await getModels(options)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}:`,
|
||||
error,
|
||||
provider.log(
|
||||
`Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
|
@ -835,14 +821,14 @@ export const webviewMessageHandler = async (
|
|||
},
|
||||
},
|
||||
{
|
||||
key: "roo" as RouterName,
|
||||
key: "roo",
|
||||
options: {
|
||||
provider: "roo" as any,
|
||||
provider: "roo",
|
||||
baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy",
|
||||
apiKey: CloudService.hasInstance()
|
||||
? CloudService.instance.authService?.getSessionToken()
|
||||
: undefined,
|
||||
} as GetModelsOptions,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -870,7 +856,10 @@ export const webviewMessageHandler = async (
|
|||
|
||||
// If nothing matched (edge case), still post empty structure for stability
|
||||
if (modelFetchPromises.length === 0) {
|
||||
await provider.postMessageToWebview({ type: "routerModels", routerModels })
|
||||
await provider.postMessageToWebview({
|
||||
type: "routerModels",
|
||||
routerModels: routerModels as RouterModels,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
|
|
@ -887,7 +876,7 @@ export const webviewMessageHandler = async (
|
|||
routerModels[routerName] = result.value.models
|
||||
} else {
|
||||
const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)
|
||||
console.error(`Error fetching models for ${routerName}:`, result.reason)
|
||||
provider.log(`Error fetching models for ${routerName}: ${errorMessage}`)
|
||||
routerModels[routerName] = {}
|
||||
provider.postMessageToWebview({
|
||||
type: "singleRouterModelFetchResponse",
|
||||
|
|
@ -898,22 +887,14 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
})
|
||||
|
||||
provider.postMessageToWebview({ type: "routerModels", routerModels })
|
||||
provider.postMessageToWebview({ type: "routerModels", routerModels: routerModels as RouterModels })
|
||||
break
|
||||
}
|
||||
case "requestRouterModelsAll": {
|
||||
// Phase 3: Debounce to collapse rapid repeats
|
||||
const now = Date.now()
|
||||
if (now - lastRouterModelsAllRequestTime < ROUTER_MODELS_DEBOUNCE_MS) {
|
||||
// Skip this request - too soon after last one
|
||||
break
|
||||
}
|
||||
lastRouterModelsAllRequestTime = now
|
||||
|
||||
// Settings and activation: fetch all providers (legacy behavior)
|
||||
const { apiConfiguration } = await provider.getState()
|
||||
|
||||
const routerModels: any = {
|
||||
const routerModels: Partial<Record<RouterName, ModelRecord>> = {
|
||||
openrouter: {},
|
||||
"vercel-ai-gateway": {},
|
||||
huggingface: {},
|
||||
|
|
@ -932,9 +913,8 @@ export const webviewMessageHandler = async (
|
|||
try {
|
||||
return await getModels(options)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to fetch models in webviewMessageHandler requestRouterModelsAll for ${options.provider}:`,
|
||||
error,
|
||||
provider.log(
|
||||
`Failed to fetch models in webviewMessageHandler requestRouterModelsAll for ${options.provider}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
|
|
@ -962,14 +942,14 @@ export const webviewMessageHandler = async (
|
|||
},
|
||||
},
|
||||
{
|
||||
key: "roo" as RouterName,
|
||||
key: "roo",
|
||||
options: {
|
||||
provider: "roo" as any,
|
||||
provider: "roo",
|
||||
baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy",
|
||||
apiKey: CloudService.hasInstance()
|
||||
? CloudService.instance.authService?.getSessionToken()
|
||||
: undefined,
|
||||
} as GetModelsOptions,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -1022,7 +1002,7 @@ export const webviewMessageHandler = async (
|
|||
} 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)
|
||||
provider.log(`Error fetching models for ${routerName}: ${errorMessage}`)
|
||||
|
||||
routerModels[routerName] = {}
|
||||
|
||||
|
|
@ -1035,7 +1015,7 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
})
|
||||
|
||||
provider.postMessageToWebview({ type: "routerModels", routerModels })
|
||||
provider.postMessageToWebview({ type: "routerModels", routerModels: routerModels as RouterModels })
|
||||
break
|
||||
}
|
||||
case "requestOllamaModels": {
|
||||
|
|
@ -1055,7 +1035,8 @@ export const webviewMessageHandler = async (
|
|||
provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: ollamaModels })
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail - user hasn't configured Ollama yet
|
||||
// Silently fail - user hasn't configured Ollama yet (debug level only)
|
||||
// Using console.debug since this is expected when Ollama isn't configured
|
||||
console.debug("Ollama models fetch failed:", error)
|
||||
}
|
||||
break
|
||||
|
|
@ -1079,7 +1060,8 @@ export const webviewMessageHandler = async (
|
|||
})
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail - user hasn't configured LM Studio yet.
|
||||
// Silently fail - user hasn't configured LM Studio yet (debug level only)
|
||||
// Using console.debug since this is expected when LM Studio isn't configured
|
||||
console.debug("LM Studio models fetch failed:", error)
|
||||
}
|
||||
break
|
||||
|
|
@ -1088,15 +1070,15 @@ export const webviewMessageHandler = async (
|
|||
// Specific handler for Roo models only - flushes cache to ensure fresh auth token is used
|
||||
try {
|
||||
// Flush cache first to ensure fresh models with current auth state
|
||||
await flushModels("roo" as RouterName)
|
||||
await flushModels("roo")
|
||||
|
||||
const rooModels = await getModels({
|
||||
provider: "roo" as any,
|
||||
provider: "roo",
|
||||
baseUrl: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy",
|
||||
apiKey: CloudService.hasInstance()
|
||||
? CloudService.instance.authService?.getSessionToken()
|
||||
: undefined,
|
||||
} as GetModelsOptions)
|
||||
})
|
||||
|
||||
// Always send a response, even if no models are returned
|
||||
provider.postMessageToWebview({
|
||||
|
|
@ -1144,7 +1126,9 @@ export const webviewMessageHandler = async (
|
|||
huggingFaceModels: huggingFaceModelsResponse.models,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch Hugging Face models:", error)
|
||||
provider.log(
|
||||
`Failed to fetch Hugging Face models: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
provider.postMessageToWebview({ type: "huggingFaceModels", huggingFaceModels: [] })
|
||||
}
|
||||
break
|
||||
|
|
@ -1463,8 +1447,7 @@ export const webviewMessageHandler = async (
|
|||
break
|
||||
case "checkpointTimeout":
|
||||
const checkpointTimeout = message.value ?? DEFAULT_CHECKPOINT_TIMEOUT_SECONDS
|
||||
// checkpointTimeout is in GlobalSettings but TypeScript inference has issues
|
||||
await provider.contextProxy.setValue("checkpointTimeout" as any, checkpointTimeout)
|
||||
await provider.contextProxy.setValue("checkpointTimeout", checkpointTimeout)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "browserViewportSize":
|
||||
|
|
@ -1849,6 +1832,14 @@ export const webviewMessageHandler = async (
|
|||
await updateGlobalState("includeTaskHistoryInEnhance", message.bool ?? true)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "includeCurrentTime":
|
||||
await updateGlobalState("includeCurrentTime", message.bool ?? true)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "includeCurrentCost":
|
||||
await updateGlobalState("includeCurrentCost", message.bool ?? true)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "condensingApiConfigId":
|
||||
await updateGlobalState("condensingApiConfigId", message.text)
|
||||
await provider.postStateToWebview()
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import {
|
|||
import { vscode } from "@src/utils/vscode"
|
||||
import { validateApiConfigurationExcludingModelErrors, getModelValidationError } from "@src/utils/validate"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
|
||||
import { useRouterModelsAll } from "@src/components/ui/hooks/useRouterModelsAll"
|
||||
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
import {
|
||||
|
|
@ -188,7 +188,7 @@ const ApiOptions = ({
|
|||
info: selectedModelInfo,
|
||||
} = useSelectedModel(apiConfiguration)
|
||||
|
||||
const { data: routerModels, refetch: refetchRouterModels } = useRouterModels()
|
||||
const { data: routerModels, refetch: refetchRouterModels } = useRouterModelsAll()
|
||||
|
||||
const { data: openRouterModelProviders } = useOpenRouterModelProviders(apiConfiguration?.openRouterModelId, {
|
||||
enabled:
|
||||
|
|
|
|||
|
|
@ -100,11 +100,11 @@ export const Unbound = ({
|
|||
window.addEventListener("message", messageHandler)
|
||||
})
|
||||
|
||||
vscode.postMessage({ type: "requestRouterModels" })
|
||||
vscode.postMessage({ type: "requestRouterModelsAll" })
|
||||
|
||||
await modelsPromise
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ["routerModels"] })
|
||||
await queryClient.invalidateQueries({ queryKey: ["routerModelsAll"] })
|
||||
|
||||
// After refreshing models, check if current model is in the updated list
|
||||
// If not, select the first available model
|
||||
|
|
|
|||
38
webview-ui/src/components/ui/hooks/useRouterModelsAll.ts
Normal file
38
webview-ui/src/components/ui/hooks/useRouterModelsAll.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
import { RouterModels } from "@roo/api"
|
||||
import { ExtensionMessage } from "@roo/ExtensionMessage"
|
||||
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
|
||||
const getRouterModelsAll = async () =>
|
||||
new Promise<RouterModels>((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
window.removeEventListener("message", handler)
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup()
|
||||
reject(new Error("Router models (all) 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: "requestRouterModelsAll" })
|
||||
})
|
||||
|
||||
export const useRouterModelsAll = () => useQuery({ queryKey: ["routerModelsAll"], queryFn: getRouterModelsAll })
|
||||
Loading…
Add table
Reference in a new issue