Compare commits

...

11 Commits

Author SHA1 Message Date
abeatrix ec34fbc0c2 add cache time 2025-11-07 17:47:05 -08:00
abeatrix 2dee236d60 ops 2025-11-07 17:29:30 -08:00
abeatrix 1ff0e95a0a Merge branch 'main' into bee/vercel-models 2025-11-07 17:07:29 -08:00
abeatrix a6334c08aa clean up 2025-11-07 17:05:49 -08:00
abeatrix 3225571b5e clean up 2025-11-07 17:00:49 -08:00
abeatrix 936b558e5f remove command.tsx 2025-11-07 16:29:41 -08:00
abeatrix 7c0468ca9d update reducer 2025-11-07 16:25:41 -08:00
abeatrix 83eff4beec use same dropdown 2025-11-07 15:16:47 -08:00
abeatrix 1f8f8c54fb add dropdown 2025-11-07 15:09:08 -08:00
abeatrix aa23448501 Create ModelContextProvider 2025-11-07 02:54:03 -08:00
abeatrix d02ae12a19 feat: add Vercel AI Gateway models support and refactor model caching
Add refreshVercelAiGatewayModelsRpc RPC endpoint and refreshClineModelsRpc to the ModelsService. Refactor OpenRouter model caching logic by extracting readOpenRouterModels method from Controller into a standalone getOpenRouterCachedModels function. Move appendClineStealthModels import to refreshClineModels module. Remove unused ModelInfo import from controller index.
2025-11-05 21:34:28 -08:00
21 changed files with 545 additions and 188 deletions
+4
View File
@@ -11,6 +11,8 @@ option java_package = "bot.cline.proto";
// Service for model-related operations
service ModelsService {
// Refreshes and returns Cline models
rpc refreshClineModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Fetches available models from Ollama
rpc getOllamaModels(StringRequest) returns (StringArray);
// Fetches available models from LM Studio
@@ -27,6 +29,8 @@ service ModelsService {
rpc refreshRequestyModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Hicap models
rpc refreshHicapModels(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Refreshes and returns Vercel AI Gateway models
rpc refreshVercelAiGatewayModelsRpc(EmptyRequest) returns (OpenRouterCompatibleModelInfo);
// Subscribe to OpenRouter models updates
rpc subscribeToOpenRouterModels(EmptyRequest) returns (stream OpenRouterCompatibleModelInfo);
// Updates API configuration (legacy - uses combined configuration)
+1 -19
View File
@@ -8,7 +8,7 @@ import { cleanupLegacyCheckpoints } from "@integrations/checkpoints/CheckpointMi
import { downloadTask } from "@integrations/misc/export-markdown"
import { ClineAccountService } from "@services/account/ClineAccountService"
import { McpHub } from "@services/mcp/McpHub"
import { ApiProvider, ModelInfo } from "@shared/api"
import { ApiProvider } from "@shared/api"
import { ChatContent } from "@shared/ChatContent"
import { ExtensionState, Platform } from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
@@ -39,7 +39,6 @@ import { getLatestAnnouncementId } from "@/utils/announcements"
import { getCwd, getDesktopDir } from "@/utils/path"
import { PromptRegistry } from "../prompts/system-prompt"
import {
ensureCacheDirectoryExists,
ensureMcpServersDirectoryExists,
ensureSettingsDirectoryExists,
GlobalFileNames,
@@ -50,7 +49,6 @@ import { PersistenceErrorEvent, StateManager } from "../storage/StateManager"
import { Task } from "../task"
import { StreamingResponseHandler } from "./grpc-handler"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { appendClineStealthModels } from "./models/refreshOpenRouterModels"
import { checkCliInstallation } from "./state/checkCliInstallation"
import { sendStateUpdate } from "./state/subscribeToState"
import { sendChatButtonClickedEvent } from "./ui/subscribeToChatButtonClicked"
@@ -748,22 +746,6 @@ export class Controller {
}
}
// Read OpenRouter models from disk cache
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
try {
if (await fileExistsAtPath(openRouterModelsFilePath)) {
const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
const models = JSON.parse(fileContents)
// Append stealth models
return appendClineStealthModels(models)
}
} catch (error) {
console.error("Error reading cached OpenRouter models:", error)
}
return undefined
}
// Task history
async getTaskWithId(id: string): Promise<{
@@ -0,0 +1,120 @@
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { ModelInfo } from "@shared/api"
import fs from "fs/promises"
import path from "path"
import { Controller } from ".."
import { refreshOpenRouterModels } from "./refreshOpenRouterModels"
import { refreshVercelAiGatewayModels } from "./refreshVercelAiGatewayModels"
type SupportedProviders = "openRouter" | "vercel_ai_gateway" | "cline"
interface ModelCache {
provider: SupportedProviders
models: Record<string, ModelInfo> | undefined
filePath: string | undefined
lastUpdated?: number | undefined
isRefreshing: boolean
}
const cache: ModelCache = {
provider: "openRouter", // Could be controlled by feature flag later
models: undefined,
filePath: undefined,
lastUpdated: undefined,
isRefreshing: false,
}
/**
* Stealth models are models that are compatible with the OpenRouter API but not listed on the OpenRouter website or API.
*/
const CLINE_STEALTH_MODELS: Record<string, ModelInfo> = {
// Add more stealth models here as needed
// Right now this list is empty as the latest stealth model was removed
}
/**
* NOTE: WIP - This function is intended to eventually make swapping between multiple Cline-compatible model providers easier.
* Core function: Refreshes Cline models from specified provider and returns application types
* @param controller The controller instance (unused)
* @returns Record of model ID to ModelInfo (application types)
*/
export async function refreshClineModels(controller: Controller): Promise<Record<string, ModelInfo>> {
cache.isRefreshing = true
let models: Record<string, ModelInfo> = {}
try {
if (cache.provider === "openRouter") {
models = await refreshOpenRouterModels(controller)
} else if (cache.provider === "vercel_ai_gateway") {
models = await refreshVercelAiGatewayModels(controller)
}
} catch (error) {
console.error("Error fetching Cline models:", error)
}
if (models && Object.keys(models).length > 0) {
try {
cache.models = appendClineStealthModels(models)
// The refresh model function has already stored the models to disk
// const filePath = await getFilePath()
// await fs.writeFile(filePath, JSON.stringify(cache.models))
// console.log("Cline models fetched and saved", JSON.stringify(models).slice(0, 300))
} catch {
throw new Error("Failed to write Cline models to disk")
}
}
cache.isRefreshing = false
return cache.models || (await getClineCachedModels()) || {}
}
// Get the file path based on the current provider
async function getFilePath() {
if (!cache.filePath) {
const cacheDir = await ensureCacheDirectoryExists()
switch (cache.provider) {
case "vercel_ai_gateway":
cache.filePath = path.join(cacheDir, GlobalFileNames.vercelAiGatewayModels)
break
case "openRouter":
cache.filePath = path.join(cacheDir, GlobalFileNames.openRouterModels)
break
case "cline":
cache.filePath = path.join(cacheDir, GlobalFileNames.clineModelsCache)
}
}
return cache.filePath
}
/**
* Reads cached Cline models from disk (application types)
*/
export async function getClineCachedModels(): Promise<Record<string, ModelInfo> | undefined> {
if (!cache.models && !cache.isRefreshing && !cache.filePath) {
try {
const filePath = await getFilePath()
if (filePath) {
cache.filePath = filePath
const content = await fs.readFile(filePath, "utf8")
const models = JSON.parse(content)
cache.models = models
}
} catch (error) {
console.error("Error reading cached Cline models:", error)
}
}
return appendClineStealthModels(cache.models || {})
}
export function appendClineStealthModels(currentModels: Record<string, ModelInfo>): Record<string, ModelInfo> {
// Create a shallow clone of the current models to avoid mutating the original object
const cloned = { ...currentModels }
for (const [modelId, modelInfo] of Object.entries(CLINE_STEALTH_MODELS)) {
if (!cloned[modelId]) {
cloned[modelId] = modelInfo
}
}
return cloned
}
@@ -0,0 +1,21 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
import { toProtobufModels } from "../../../shared/proto-conversions/models/typeConversion"
import type { Controller } from "../index"
import { refreshClineModels } from "./refreshClineModels"
/**
* Refreshes OpenRouter models and returns protobuf types for gRPC
* @param controller The controller instance
* @param request Empty request (unused but required for gRPC signature)
* @returns OpenRouterCompatibleModelInfo with protobuf types
*/
export async function refreshClineModelsRpc(
controller: Controller,
_request: EmptyRequest,
): Promise<OpenRouterCompatibleModelInfo> {
const models = await refreshClineModels(controller)
return OpenRouterCompatibleModelInfo.create({
models: toProtobufModels(models),
})
}
@@ -10,7 +10,9 @@ import {
openRouterClaudeSonnet41mModelId,
openRouterClaudeSonnet451mModelId,
} from "@/shared/api"
import { fileExistsAtPath } from "@/utils/fs"
import type { Controller } from ".."
import { appendClineStealthModels } from "./refreshClineModels"
type OpenRouterSupportedParams =
| "frequency_penalty"
@@ -74,7 +76,7 @@ interface OpenRouterRawModelInfo {
* @param controller The controller instance
* @returns Record of model ID to ModelInfo (application types)
*/
export async function refreshOpenRouterModels(controller: Controller): Promise<Record<string, ModelInfo>> {
export async function refreshOpenRouterModels(_controller: Controller): Promise<Record<string, ModelInfo>> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
const models: Record<string, ModelInfo> = {}
@@ -239,7 +241,7 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
console.error("Error fetching OpenRouter models:", error)
// If we failed to fetch models, try to read cached models
const cachedModels = await controller.readOpenRouterModels()
const cachedModels = await getOpenRouterCachedModels()
if (cachedModels) {
// Cached models are already in application format (ModelInfo)
return appendClineStealthModels(cachedModels as Record<string, ModelInfo>)
@@ -249,21 +251,17 @@ export async function refreshOpenRouterModels(controller: Controller): Promise<R
return appendClineStealthModels(models)
}
/**
* Stealth models are models that are compatible with the OpenRouter API but not listed on the OpenRouter website or API.
*/
const CLINE_STEALTH_MODELS: Record<string, ModelInfo> = {
// Add more stealth models here as needed
// Right now this list is empty as the latest stealth model was removed
}
export function appendClineStealthModels(currentModels: Record<string, ModelInfo>): Record<string, ModelInfo> {
// Create a shallow clone of the current models to avoid mutating the original object
const cloned = { ...currentModels }
for (const [modelId, modelInfo] of Object.entries(CLINE_STEALTH_MODELS)) {
if (!cloned[modelId]) {
cloned[modelId] = modelInfo
async function getOpenRouterCachedModels(): Promise<Record<string, ModelInfo> | undefined> {
const openRouterModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.openRouterModels)
try {
if (await fileExistsAtPath(openRouterModelsFilePath)) {
const fileContents = await fs.readFile(openRouterModelsFilePath, "utf8")
const models = JSON.parse(fileContents)
// Append stealth models
return appendClineStealthModels(models)
}
} catch (error) {
console.error("Error reading cached OpenRouter models:", error)
}
return cloned
return undefined
}
@@ -1,66 +1,110 @@
import { ensureCacheDirectoryExists, GlobalFileNames } from "@core/storage/disk"
import { ModelInfo } from "@shared/api"
import { fileExistsAtPath } from "@utils/fs"
import axios from "axios"
import fs from "fs/promises"
import path from "path"
import { Controller } from ".."
interface VercelAiGatewayRawModelInfo {
id: string
name: string
object: "model"
created: number
owned_by: string // e.g amazon, google, anthropic, openai, etc.
description: string | null
context_window: number | null
max_tokens: number | null
type: "embedding" | "language" | string
tags?: ("file-input" | "reasoning" | "implicit-caching" | "tool-use" | "vision" | "image-generation" | string)[]
pricing?: {
input?: string | null
output?: string | null
input_cache_read?: string | null
input_cache_write?: string | null
} | null
}
const VERCEL_AI_GATEWAY_MODELS_URL = "https://ai-gateway.vercel.sh/v1/models"
const REFRESH_INTERVAL_MS = 1000 * 60 * 60 // 1 hour
let cache: Record<string, ModelInfo> | undefined
let lastRefreshTimestamp = 0
/**
* Core function: Refreshes Vercel AI Gateway models and returns application types
* @param _controller The controller instance (unused)
* @returns Record of model ID to ModelInfo (application types)
*/
export async function refreshVercelAiGatewayModels(_controller: Controller): Promise<Record<string, ModelInfo>> {
// If last refresh was within the interval, return cached models
if (cache && Date.now() - lastRefreshTimestamp < REFRESH_INTERVAL_MS) {
return cache
}
const vercelAiGatewayModelsFilePath = path.join(await ensureCacheDirectoryExists(), GlobalFileNames.vercelAiGatewayModels)
let models: Record<string, ModelInfo> = {}
const models: Record<string, ModelInfo> | undefined = cache || (await readVercelAiGatewayModels()) || {}
try {
const response = await axios.get("https://ai-gateway.vercel.sh/v1/models")
const response = await fetch(VERCEL_AI_GATEWAY_MODELS_URL, {
method: "GET",
})
if (response.data?.data) {
const rawModels = response.data.data
const parsePrice = (price: any) => {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
for (const rawModel of rawModels) {
if (rawModel.type === "embedding") {
continue
}
const modelInfo: ModelInfo = {
maxTokens: rawModel.max_tokens ?? 0,
contextWindow: rawModel.context_window ?? 0,
inputPrice: parsePrice(rawModel.pricing?.input) ?? 0,
outputPrice: parsePrice(rawModel.pricing?.output) ?? 0,
cacheWritesPrice: parsePrice(rawModel.pricing?.input_cache_write) ?? 0,
cacheReadsPrice: parsePrice(rawModel.pricing?.input_cache_read) ?? 0,
supportsImages: true, // assume all models support images since vercel ai doesn't give this info
supportsPromptCache: !!(rawModel.pricing?.input_cache_read && rawModel.pricing?.input_cache_write),
description: rawModel.description ?? "",
}
models[rawModel.id] = modelInfo
}
await fs.writeFile(vercelAiGatewayModelsFilePath, JSON.stringify(models))
console.log("Vercel AI Gateway models fetched and saved", JSON.stringify(models).slice(0, 300))
} else {
console.error("Invalid response from Vercel AI Gateway API")
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Vercel Gateway error: ${response.status} ${response.statusText} - ${errorText}`)
}
const data = await response.json()
const rawModels = data?.data as VercelAiGatewayRawModelInfo[]
for (const raw of rawModels) {
if (raw.type !== "language") {
continue
}
const modelInfo: ModelInfo = {
maxTokens: raw.max_tokens ?? 0,
contextWindow: raw.context_window ?? 0,
inputPrice: parsePrice(raw.pricing?.input),
outputPrice: parsePrice(raw.pricing?.output),
cacheWritesPrice: parsePrice(raw.pricing?.input_cache_write),
cacheReadsPrice: parsePrice(raw.pricing?.input_cache_read),
supportsImages: raw.tags?.some((tag) => tag === "vision") ?? false,
supportsPromptCache: raw.tags?.some((tag) => tag === "implicit-caching") ?? false,
description: raw.description ?? `${raw.name} by ${raw.owned_by}`,
}
if (modelInfo.cacheReadsPrice || modelInfo.cacheWritesPrice) {
modelInfo.supportsPromptCache = true
}
if (modelInfo.maxTokens && raw.tags?.some((tag) => tag === "reasoning")) {
modelInfo.thinkingConfig = {
// Allocate max 20% of max tokens for reasoning/thinking
maxBudget: modelInfo.maxTokens * 0.2,
}
}
models[raw.id] = modelInfo
}
// Update last refresh timestamp & cache the models in memory and in disk
lastRefreshTimestamp = Date.now()
cache = models
console.log("Vercel AI Gateway models refreshed from network")
try {
await fs.writeFile(vercelAiGatewayModelsFilePath, JSON.stringify(models))
console.log("Vercel AI Gateway models written to disk cache")
} catch {
throw new Error("Failed to write Vercel AI Gateway models to disk")
}
return models
} catch (error) {
console.error("Error fetching Vercel AI Gateway models:", error)
// If we failed to fetch models, try to read cached models
const cachedModels = await readVercelAiGatewayModels()
if (cachedModels) {
models = cachedModels
}
}
return models
@@ -83,3 +127,7 @@ async function readVercelAiGatewayModels(): Promise<Record<string, ModelInfo> |
}
return undefined
}
const parsePrice = (price?: string | null) => {
return price !== null && price !== undefined ? parseFloat(price) * 1_000_000 : undefined
}
@@ -1,7 +1,7 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import type { EmptyRequest } from "@shared/proto/cline/common"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
import { toProtobufModels } from "../../../shared/proto-conversions/models/typeConversion"
import { Controller } from ".."
import type { Controller } from ".."
import { refreshVercelAiGatewayModels } from "./refreshVercelAiGatewayModels"
/**
+5 -6
View File
@@ -6,9 +6,9 @@ import { GlobalStateAndSettings } from "@/shared/storage/state-keys"
import type { Controller } from "../index"
import { sendMcpMarketplaceCatalogEvent } from "../mcp/subscribeToMcpMarketplaceCatalog"
import { refreshBasetenModels } from "../models/refreshBasetenModels"
import { getClineCachedModels, refreshClineModels } from "../models/refreshClineModels"
import { refreshGroqModels } from "../models/refreshGroqModels"
import { refreshHicapModels } from "../models/refreshHicapModels"
import { refreshOpenRouterModels } from "../models/refreshOpenRouterModels"
import { sendOpenRouterModelsEvent } from "../models/subscribeToOpenRouterModels"
/**
@@ -20,13 +20,12 @@ import { sendOpenRouterModelsEvent } from "../models/subscribeToOpenRouterModels
export async function initializeWebview(controller: Controller, _request: EmptyRequest): Promise<Empty> {
try {
// Post last cached models as soon as possible for immediate availability in the UI
const lastCachedModels = await controller.readOpenRouterModels()
if (lastCachedModels) {
sendOpenRouterModelsEvent(OpenRouterCompatibleModelInfo.create({ models: lastCachedModels }))
}
await getClineCachedModels().then(async (cache) =>
sendOpenRouterModelsEvent(OpenRouterCompatibleModelInfo.create({ models: cache })),
)
// Refresh OpenRouter models from API
refreshOpenRouterModels(controller).then(async (models) => {
refreshClineModels(controller).then(async (models) => {
if (models && Object.keys(models).length > 0) {
// Update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there)
const apiConfiguration = controller.stateManager.getApiConfiguration()
+1
View File
@@ -19,6 +19,7 @@ export const GlobalFileNames = {
uiMessages: "ui_messages.json",
openRouterModels: "openrouter_models.json",
vercelAiGatewayModels: "vercel_ai_gateway_models.json",
clineModelsCache: "cline_models_cache.json",
groqModels: "groq_models.json",
basetenModels: "baseten_models.json",
hicapModels: "hicap_models.json",
+5
View File
@@ -1,5 +1,6 @@
import type { Boolean, EmptyRequest } from "@shared/proto/cline/common"
import { useEffect } from "react"
import { useMount } from "react-use"
import AccountView from "./components/account/AccountView"
import ChatView from "./components/chat/ChatView"
import HistoryView from "./components/history/HistoryView"
@@ -8,6 +9,7 @@ import OnboardingView from "./components/onboarding/OnboardingView"
import SettingsView from "./components/settings/SettingsView"
import { useClineAuth } from "./context/ClineAuthContext"
import { useExtensionState } from "./context/ExtensionStateContext"
import { ModelRefreshProvider, useModelContext } from "./context/ModelContext"
import { Providers } from "./Providers"
import { UiServiceClient } from "./services/grpc-client"
@@ -33,6 +35,9 @@ const AppContent = () => {
} = useExtensionState()
const { clineUser, organizations, activeOrganization } = useClineAuth()
const { refreshModels } = useModelContext()
useMount(() => refreshModels(ModelRefreshProvider.Cline))
useEffect(() => {
if (shouldShowAnnouncement) {
+6 -3
View File
@@ -3,6 +3,7 @@ import { type ReactNode } from "react"
import { CustomPostHogProvider } from "./CustomPostHogProvider"
import { ClineAuthProvider } from "./context/ClineAuthContext"
import { ExtensionStateContextProvider } from "./context/ExtensionStateContext"
import { ModelContextProvider } from "./context/ModelContext"
import { PlatformProvider } from "./context/PlatformContext"
export function Providers({ children }: { children: ReactNode }) {
@@ -10,9 +11,11 @@ export function Providers({ children }: { children: ReactNode }) {
<PlatformProvider>
<ExtensionStateContextProvider>
<CustomPostHogProvider>
<ClineAuthProvider>
<HeroUIProvider>{children}</HeroUIProvider>
</ClineAuthProvider>
<ModelContextProvider>
<ClineAuthProvider>
<HeroUIProvider>{children}</HeroUIProvider>
</ClineAuthProvider>
</ModelContextProvider>
</CustomPostHogProvider>
</ExtensionStateContextProvider>
</PlatformProvider>
@@ -1,9 +1,7 @@
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { XIcon } from "lucide-react"
import { CSSProperties, memo } from "react"
import { useMount } from "react-use"
import { Button } from "@/components/ui/button"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND, VSC_INACTIVE_SELECTION_BACKGROUND } from "@/utils/vscStyles"
interface AnnouncementProps {
@@ -38,9 +36,6 @@ Patch releases (3.19.1 → 3.19.2) will not trigger new announcements.
*/
const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
const minorVersion = version.split(".").slice(0, 2).join(".") // 2.0.0 -> 2.0
const { refreshOpenRouterModels } = useExtensionState()
// Need to get latest model list in case user hits shortcut button to set model
useMount(refreshOpenRouterModels)
return (
<div style={containerStyle}>
@@ -23,6 +23,7 @@ import { getModeSpecificFields, normalizeApiConfiguration } from "@/components/s
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { useClineAuth } from "@/context/ClineAuthContext"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useModelContext } from "@/context/ModelContext"
import { usePlatform } from "@/context/PlatformContext"
import { cn } from "@/lib/utils"
import { FileServiceClient, ModelsServiceClient, StateServiceClient } from "@/services/grpc-client"
@@ -263,7 +264,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const {
mode,
apiConfiguration,
openRouterModels,
platform,
localWorkflowToggles,
globalWorkflowToggles,
@@ -271,6 +271,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
setShowChatModelSelector: setShowModelSelector,
dictationSettings,
} = useExtensionState()
const { models } = useModelContext()
const { clineUser } = useClineAuth()
const [isTextAreaFocused, setIsTextAreaFocused] = useState(false)
const [isDraggingOver, setIsDraggingOver] = useState(false)
@@ -1020,7 +1021,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
// Separate the API config submission logic
const submitApiConfig = useCallback(async () => {
const apiValidationResult = validateApiConfiguration(mode, apiConfiguration)
const modelIdValidationResult = validateModelId(mode, apiConfiguration, openRouterModels)
const modelIdValidationResult = validateModelId(mode, apiConfiguration, models.openRouter)
if (!apiValidationResult && !modelIdValidationResult && apiConfiguration) {
try {
@@ -1041,7 +1042,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
console.error("Error refreshing state:", error)
})
}
}, [apiConfiguration, openRouterModels])
}, [apiConfiguration, models])
const onModeToggle = useCallback(() => {
// if (textAreaDisabled) return
@@ -1,10 +1,10 @@
import { EmptyRequest, Int64Request } from "@shared/proto/index.cline"
import { Megaphone, XIcon } from "lucide-react"
import { useCallback } from "react"
import { useMount } from "react-use"
import { Button } from "@/components/ui/button"
import { useClineAuth } from "@/context/ClineAuthContext"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useModelContext } from "@/context/ModelContext"
import { AccountServiceClient, StateServiceClient } from "@/services/grpc-client"
import { getAsVar, VSC_INACTIVE_SELECTION_BACKGROUND } from "@/utils/vscStyles"
import { useApiConfigurationHandlers } from "../settings/utils/useApiConfigurationHandlers"
@@ -13,12 +13,10 @@ export const CURRENT_MODEL_BANNER_VERSION = 2
export const NewModelBanner: React.FC = () => {
const { clineUser } = useClineAuth()
const { openRouterModels, setShowChatModelSelector, refreshOpenRouterModels } = useExtensionState()
const { setShowChatModelSelector } = useExtensionState()
const user = clineUser || undefined
const { handleFieldsChange } = useApiConfigurationHandlers()
// Need to get latest model list in case user hits shortcut button to set model
useMount(refreshOpenRouterModels)
const { models } = useModelContext()
const handleClose = useCallback((e?: React.MouseEvent) => {
e?.preventDefault()
@@ -36,8 +34,8 @@ export const NewModelBanner: React.FC = () => {
handleFieldsChange({
planModeOpenRouterModelId: modelId,
actModeOpenRouterModelId: modelId,
planModeOpenRouterModelInfo: openRouterModels[modelId],
actModeOpenRouterModelInfo: openRouterModels[modelId],
planModeOpenRouterModelInfo: models.openRouter[modelId],
actModeOpenRouterModelInfo: models.openRouter[modelId],
planModeApiProvider: "cline",
actModeApiProvider: "cline",
})
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Item, ItemContent, ItemDescription, ItemHeader, ItemMedia, ItemTitle } from "@/components/ui/item"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useModelContext } from "@/context/ModelContext"
import { cn } from "@/lib/utils"
import { AccountServiceClient, StateServiceClient } from "@/services/grpc-client"
import ApiConfigurationSection from "../settings/sections/ApiConfigurationSection"
@@ -230,7 +231,9 @@ const OnboardingStepContent = ({
const OnboardingView = () => {
const { handleFieldsChange } = useApiConfigurationHandlers()
const { openRouterModels, hideSettings, hideAccount, setShowWelcome } = useExtensionState()
const { hideSettings, hideAccount, setShowWelcome } = useExtensionState()
const { models } = useModelContext()
const openRouterModels = useMemo(() => models.openRouter, [models])
const [stepNumber, setStepNumber] = useState(0)
const [userType, setUserType] = useState<NEW_USER_TYPE>(NEW_USER_TYPE.FREE)
@@ -8,6 +8,7 @@ import { type KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"
import { useMount } from "react-use"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelRefreshProvider, useModelContext } from "@/context/ModelContext"
import { StateServiceClient } from "@/services/grpc-client"
import { highlight } from "../history/HistoryView"
import { ContextWindowSwitcher } from "./common/ContextWindowSwitcher"
@@ -66,8 +67,14 @@ const FREE_CLINE_MODELS = featuredModels.filter((m) => m.isFree)
const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup, currentMode }) => {
const { handleModeFieldsChange } = useApiConfigurationHandlers()
const { apiConfiguration, favoritedModelIds, openRouterModels, refreshOpenRouterModels } = useExtensionState()
const { apiConfiguration, favoritedModelIds } = useExtensionState()
const { models, refreshModels } = useModelContext()
const modeFields = getModeSpecificFields(apiConfiguration, currentMode)
// Determine which provider to use based on the current API provider
const PROVIDER_ID = modeFields.apiProvider === "cline" ? ModelRefreshProvider.Cline : ModelRefreshProvider.OpenRouter
const openRouterModels = models[PROVIDER_ID]
const [searchTerm, setSearchTerm] = useState(modeFields.openRouterModelId || openRouterDefaultModelId)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -87,7 +94,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup,
},
{
openRouterModelId: newModelId,
openRouterModelInfo: openRouterModels[newModelId],
openRouterModelInfo: models[PROVIDER_ID][newModelId],
},
currentMode,
)
@@ -112,7 +119,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup,
return selected
}, [apiConfiguration, currentMode])
useMount(refreshOpenRouterModels)
useMount(() => refreshModels(PROVIDER_ID))
// Sync external changes when the modelId changes
useEffect(() => {
@@ -261,7 +268,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup,
selectedModelId?.toLowerCase().includes("claude-3.7-sonnet") ||
selectedModelId?.toLowerCase().includes("claude-3.7-sonnet:thinking")
)
}, [selectedModelId])
}, [selectedModelId, openRouterModels])
return (
<div style={{ width: "100%" }}>
@@ -1,8 +1,15 @@
import { Mode } from "@shared/storage/types"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import type { Mode } from "@shared/storage/types"
import { VSCodeDropdown, VSCodeLink, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useMemo } from "react"
import { useMount } from "react-use"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelRefreshProvider, useModelContext } from "@/context/ModelContext"
import { ContextWindowSwitcher } from "../common/ContextWindowSwitcher"
import { DebouncedTextField } from "../common/DebouncedTextField"
import OpenRouterModelPicker from "../OpenRouterModelPicker"
import { ModelInfoView } from "../common/ModelInfoView"
import { DropdownContainer } from "../common/ModelSelector"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
@@ -14,49 +21,134 @@ interface VercelAIGatewayProviderProps {
currentMode: Mode
}
const PROVIDER_ID = ModelRefreshProvider.VercelAIGateway
/**
* The Vercel AI Gateway provider configuration component
*/
export const VercelAIGatewayProvider = ({ showModelOptions, isPopup, currentMode }: VercelAIGatewayProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange, handleModeFieldsChange } = useApiConfigurationHandlers()
const { models, refreshModels } = useModelContext()
const { vercelModelIds, selectedModelId, selectedModelInfo } = useMemo(() => {
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration, currentMode)
const vercelModels = models[PROVIDER_ID]
const vercelModelIds = Object.keys(vercelModels)
return {
apiConfiguration,
vercelModels,
vercelModelIds,
selectedModelInfo,
selectedModelId,
}
}, [models, apiConfiguration, currentMode])
useMount(() => refreshModels(PROVIDER_ID))
const handleModelChange = (newModelId: string) => {
// could be setting invalid model id/undefined info but validation will catch it
handleModeFieldsChange(
{
openRouterModelId: { plan: "planModeOpenRouterModelId", act: "actModeOpenRouterModelId" },
openRouterModelInfo: { plan: "planModeOpenRouterModelInfo", act: "actModeOpenRouterModelInfo" },
},
{
openRouterModelId: newModelId,
openRouterModelInfo: models[PROVIDER_ID][newModelId],
},
currentMode,
)
}
return (
<div>
<div>
<div className="w-full h-full relative" id="vercel-ai-gateway-provider">
<div className="w-full">
<DebouncedTextField
className="w-full"
initialValue={apiConfiguration?.vercelAiGatewayApiKey || ""}
onChange={(value) => handleFieldChange("vercelAiGatewayApiKey", value)}
placeholder="Enter API Key..."
style={{ width: "100%" }}
type="password">
<span style={{ fontWeight: 500 }}>Vercel AI Gateway API Key</span>
<span className="font-semibold">Vercel AI Gateway API Key</span>
</DebouncedTextField>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
<p className="mt-0 text-description text-sm">
This key is stored locally and only used to make API requests from this extension.
{!apiConfiguration?.vercelAiGatewayApiKey && (
<>
{" "}
You can get a Vercel AI Gateway API key by{" "}
<span className="mx-0.5">
You can get a Vercel AI Gateway API key by
<VSCodeLink
href="https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai"
style={{ display: "inline", fontSize: "inherit" }}>
className="inline text-link text-sm mx-0.5"
href="https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai">
signing up here.
</VSCodeLink>
</>
</span>
)}
</p>
</div>
{showModelOptions && (
<>
<OpenRouterModelPicker currentMode={currentMode} isPopup={isPopup} />
</>
<div className="w-full vercel-dropdown-container">
<div className="w-full flex flex-col">
<span className="font-semibold">Model</span>
<DropdownContainer className="vercel-dropdown-container" zIndex={1000}>
<VSCodeDropdown
className="w-full mt-2"
data-testid="vercel-model-selector"
onChange={(e) => {
const target = e.target as HTMLSelectElement
if (target) {
handleModelChange(target?.value)
}
}}
value={selectedModelId}>
{vercelModelIds.map((model) => (
<VSCodeOption className="p-1 px-2 w-full" key={"vercel" + model} value={model}>
<div className="py-1 px-1 flex justify-between w-full items-center">
<div className="break-words whitespace-normal max-w-full">{model}</div>
</div>
</VSCodeOption>
))}
</VSCodeDropdown>
</DropdownContainer>
{/* Context window switcher for Claude Sonnet 4.5 */}
<ContextWindowSwitcher
base1mModelId={"anthropic/claude-sonnet-4:1m"}
base200kModelId="anthropic/claude-sonnet-4.5"
onModelChange={handleModelChange}
selectedModelId={selectedModelId}
/>
{/* Context window switcher for Claude Sonnet 4 */}
<ContextWindowSwitcher
base1mModelId={"anthropic/claude-sonnet-4:1m"}
base200kModelId="anthropic/claude-sonnet-4"
onModelChange={handleModelChange}
selectedModelId={selectedModelId}
/>
</div>
{selectedModelInfo?.thinkingConfig?.maxBudget && <ThinkingBudgetSlider currentMode={currentMode} />}
{selectedModelId && selectedModelInfo ? (
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
) : (
<p className="mt-1 text-description text-sm">
The extension automatically fetches the latest available models from
<VSCodeLink className="inline mx-0.5 text-sm" href="https://vercel.com/ai-gateway/models">
Vercel AI Gateway.
</VSCodeLink>
If you're unsure which model to choose, Cline works best with
<VSCodeLink
className="inline mx-0.5 text-sm"
onClick={() => handleModelChange("anthropic/claude-sonnet-4.5")}>
anthropic/claude-sonnet-4.5.
</VSCodeLink>
</p>
)}
</div>
)}
</div>
)
+13 -8
View File
@@ -13,9 +13,10 @@ function PopoverContent({
className,
align = "center",
sideOffset = 4,
showArrow = true,
children,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
}: React.ComponentProps<typeof PopoverPrimitive.Content> & { showArrow?: boolean }) {
// Get side prop for conditional arrow positioning
const side = (props as any).side
@@ -24,19 +25,23 @@ function PopoverContent({
<PopoverPrimitive.Content
align={align}
className={cn(
"bg-menu text-base text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-xs border p-2 shadow-md outline-hidden border-menu-foreground/10",
"bg-menu text-base text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 w-full origin-(--radix-popover-content-transform-origin) rounded-sm border p-2 shadow-md outline-hidden border-menu-foreground/10 z-1000",
className,
)}
data-slot="popover-content"
sideOffset={sideOffset}
{...props}>
{children}
<PopoverPrimitive.Arrow
className={cn(
"bg-menu fill-menu z-50 size-2.5 rotate-45 rounded-xs border-b border-r border-menu-foreground/10",
side === "left" || side === "right" ? "translate-x-[calc(-50%_-_2px)]" : "translate-y-[calc(-50%_-_2px)]",
)}
/>
{showArrow && (
<PopoverPrimitive.Arrow
className={cn(
"text-popover fill-menu z-50 size-2.5 rotate-45 rounded-sm border-b border-r border-menu-foreground/10",
side === "left" || side === "right"
? "translate-x-[calc(-50%_-_2px)]"
: "translate-y-[calc(-50%_-_2px)]",
)}
/>
)}
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
)
@@ -12,7 +12,6 @@ import type { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
import { type TerminalProfile } from "@shared/proto/cline/state"
import { convertProtoToClineMessage } from "@shared/proto-conversions/cline-message"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { fromProtobufModels } from "@shared/proto-conversions/models/typeConversion"
import type React from "react"
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"
import { Environment } from "../../../src/config"
@@ -22,8 +21,6 @@ import {
groqDefaultModelId,
groqModels,
type ModelInfo,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
} from "../../../src/shared/api"
@@ -33,13 +30,14 @@ import { McpServiceClient, ModelsServiceClient, StateServiceClient, UiServiceCli
export interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
showWelcome: boolean
openRouterModels: Record<string, ModelInfo>
hicapModels: Record<string, ModelInfo>
openAiModels: string[]
requestyModels: Record<string, ModelInfo>
groqModels: Record<string, ModelInfo>
basetenModels: Record<string, ModelInfo>
huggingFaceModels: Record<string, ModelInfo>
mcpServers: McpServer[]
mcpMarketplaceCatalog: McpMarketplaceCatalog
totalTasksSize: number | null
@@ -80,7 +78,6 @@ export interface ExtensionStateContextType extends ExtensionState {
setShowWelcome: (value: boolean) => void
// Refresh functions
refreshOpenRouterModels: () => void
refreshHicapModels: () => void
setUserInfo: (userInfo?: UserInfo) => void
@@ -236,9 +233,6 @@ export const ExtensionStateContextProvider: React.FC<{
const [expandTaskHeader, setExpandTaskHeader] = useState(true)
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
const [hicapModels, setHicapModels] = useState<Record<string, ModelInfo>>({})
const [totalTasksSize, setTotalTasksSize] = useState<number | null>(null)
const [availableTerminalProfiles, setAvailableTerminalProfiles] = useState<TerminalProfile[]>([])
@@ -269,8 +263,6 @@ export const ExtensionStateContextProvider: React.FC<{
const settingsButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const partialMessageUnsubscribeRef = useRef<(() => void) | null>(null)
const mcpMarketplaceUnsubscribeRef = useRef<(() => void) | null>(null)
const openRouterModelsUnsubscribeRef = useRef<(() => void) | null>(null)
const hicapModelsUnsubscribeRef = useRef<(() => void) | null>(null)
const workspaceUpdatesUnsubscribeRef = useRef<(() => void) | null>(null)
const relinquishControlUnsubscribeRef = useRef<(() => void) | null>(null)
@@ -481,24 +473,6 @@ export const ExtensionStateContextProvider: React.FC<{
},
})
// Subscribe to OpenRouter models updates
openRouterModelsUnsubscribeRef.current = ModelsServiceClient.subscribeToOpenRouterModels(EmptyRequest.create({}), {
onResponse: (response: OpenRouterCompatibleModelInfo) => {
console.log("[DEBUG] Received OpenRouter models update from gRPC stream")
const models = fromProtobufModels(response.models)
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...models,
})
},
onError: (error) => {
console.error("Error in OpenRouter models subscription:", error)
},
onComplete: () => {
console.log("OpenRouter models subscription completed")
},
})
// Initialize webview using gRPC
UiServiceClient.initializeWebview(EmptyRequest.create({}))
.then(() => {
@@ -595,10 +569,6 @@ export const ExtensionStateContextProvider: React.FC<{
mcpMarketplaceUnsubscribeRef.current()
mcpMarketplaceUnsubscribeRef.current = null
}
if (openRouterModelsUnsubscribeRef.current) {
openRouterModelsUnsubscribeRef.current()
openRouterModelsUnsubscribeRef.current = null
}
if (workspaceUpdatesUnsubscribeRef.current) {
workspaceUpdatesUnsubscribeRef.current()
workspaceUpdatesUnsubscribeRef.current = null
@@ -622,18 +592,6 @@ export const ExtensionStateContextProvider: React.FC<{
}
}, [])
const refreshOpenRouterModels = useCallback(() => {
ModelsServiceClient.refreshOpenRouterModelsRpc(EmptyRequest.create({}))
.then((response: OpenRouterCompatibleModelInfo) => {
const models = fromProtobufModels(response.models)
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...models,
})
})
.catch((error: Error) => console.error("Failed to refresh OpenRouter models:", error))
}, [])
const refreshHicapModels = useCallback(() => {
ModelsServiceClient.refreshHicapModels(EmptyRequest.create({}))
.then((response: OpenRouterCompatibleModelInfo) => {
@@ -649,7 +607,6 @@ export const ExtensionStateContextProvider: React.FC<{
...state,
didHydrateState,
showWelcome,
openRouterModels,
hicapModels,
openAiModels,
requestyModels,
@@ -737,7 +694,6 @@ export const ExtensionStateContextProvider: React.FC<{
})),
setMcpTab,
setTotalTasksSize,
refreshOpenRouterModels,
refreshHicapModels,
onRelinquishControl,
setUserInfo: (userInfo?: UserInfo) => setState((prevState) => ({ ...prevState, userInfo })),
+112
View File
@@ -0,0 +1,112 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import type { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
import { fromProtobufModels } from "@shared/proto-conversions/models/typeConversion"
import type React from "react"
import { createContext, useCallback, useContext, useEffect, useReducer } from "react"
import { type ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../../src/shared/api"
import { ModelsServiceClient } from "../services/grpc-client"
export enum ModelRefreshProvider {
OpenRouter = "openRouter",
Cline = "cline",
VercelAIGateway = "vercel-ai-gateway",
}
type ProviderModelContext = {
[key in ModelRefreshProvider]: Record<string, ModelInfo>
}
interface ModelContextType {
refreshModels: (provider: ModelRefreshProvider) => void
models: ProviderModelContext
}
interface ModelContextAction {
provider: ModelRefreshProvider
models: Record<string, ModelInfo>
}
// in case the extension sent a model list without the default model
const DefaultModel = { [openRouterDefaultModelId]: openRouterDefaultModelInfo }
export const ModelContext = createContext<ModelContextType | undefined>(undefined)
export const ModelContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
// Use reducer state to manage models by provider with action
const [cachedModels, dispatch] = useReducer(
(state: ProviderModelContext, action: ModelContextAction) => {
return {
...state,
[action.provider]: action.models,
}
},
{
// in case the extension sent a model list without the default model
[ModelRefreshProvider.OpenRouter]: DefaultModel,
[ModelRefreshProvider.Cline]: DefaultModel,
[ModelRefreshProvider.VercelAIGateway]: DefaultModel,
},
)
const refreshModels = useCallback((provider: ModelRefreshProvider = ModelRefreshProvider.OpenRouter) => {
let refreshPromise: Promise<OpenRouterCompatibleModelInfo>
if (provider === ModelRefreshProvider.OpenRouter) {
refreshPromise = ModelsServiceClient.refreshOpenRouterModelsRpc(EmptyRequest.create({}))
} else if (provider === ModelRefreshProvider.VercelAIGateway) {
refreshPromise = ModelsServiceClient.refreshVercelAiGatewayModelsRpc(EmptyRequest.create({}))
} else {
refreshPromise = ModelsServiceClient.refreshClineModelsRpc(EmptyRequest.create({}))
}
refreshPromise
.then((response: OpenRouterCompatibleModelInfo) => {
dispatch({
provider,
models: {
...DefaultModel,
...fromProtobufModels(response.models),
},
})
})
.catch((error: Error) => console.error(`Failed to refresh ${provider} models:`, error))
}, [])
// Handle auth status update events
useEffect(() => {
const cancelSubscription = ModelsServiceClient.subscribeToOpenRouterModels(
{},
{
onResponse: (response: OpenRouterCompatibleModelInfo) => {
dispatch({
provider: ModelRefreshProvider.OpenRouter,
models: {
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...fromProtobufModels(response.models),
},
})
},
onError: (error) => {
console.error("Error in OpenRouter models subscription:", error)
},
onComplete: () => {
console.log("OpenRouter models subscription completed")
},
},
)
// Cleanup function to cancel subscription when component unmounts
return () => {
cancelSubscription()
}
}, [])
return <ModelContext.Provider value={{ models: cachedModels, refreshModels }}>{children}</ModelContext.Provider>
}
export const useModelContext = () => {
const context = useContext(ModelContext)
if (context === undefined) {
throw new Error("useModelContext must be used within a ModelContextProvider")
}
return context
}
+9 -2
View File
@@ -3,6 +3,7 @@
--color-background: var(--vscode-sideBar-background);
--color-border: var(--vscode-focusBorder);
--color-border-panel: var(--vscode-panel-border);
--color-separator: var(--vscode-textSeparator-foreground);
--color-foreground: var(--vscode-foreground);
--color-shadow: var(--vscode-widget-shadow);
--color-code: var(--vscode-editor-background);
@@ -32,11 +33,18 @@
--color-menu-foreground: var(--vscode-menu-foreground);
--color-menu-border: var(--vscode-menu-border);
--color-menu-shadow: var(--vscode-menu-shadow);
--color-accent: var(--vscode-contrastActiveBorder);
--color-popover: var(--vscode-dropdown-background);
--color-popover-foreground: var(--vscode-dropdown-foreground);
--color-popover-border: var(--vscode-dropdown-border);
--color-popover-list: var(--vscode-dropdown-listBackground);
--color-link: var(--vscode-textLink-foreground);
--color-link-hover: var(--vscode-textLink-activeForeground);
--color-list-hover: var(--vscode-list-hoverBackground);
--color-badge-foreground: var(--vscode-badge-foreground);
--color-badge-background: var(--vscode-badge-background);
--color-badge-error-background: var(--vscode-activityErrorBadge-background);
--color-badge-error-foreground: var(--vscode-activityErrorBadge-foreground);
--color-banner-background: var(--vscode-banner-background);
--color-banner-foreground: var(--vscode-banner-foreground);
--color-banner-icon: var(--vscode-banner-iconForeground);
@@ -47,6 +55,7 @@
--color-description: var(--vscode-descriptionForeground);
--color-success: var(--vscode-charts-green);
--color-warning: var(--vscode-charts-yellow);
--color-info: var(--vscode-inputValidation-infoBorder, --vscode-charts-blue);
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--font-azeret-mono: Azeret Mono, monospace;
--text-2xl: calc(2.25 * var(--vscode-font-size));
@@ -69,8 +78,6 @@
--radius-xl: calc(var(--radius) + 4px);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);