mirror of
https://github.com/cline/cline.git
synced 2026-09-02 07:42:19 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec34fbc0c2 | |||
| 2dee236d60 | |||
| 1ff0e95a0a | |||
| a6334c08aa | |||
| 3225571b5e | |||
| 936b558e5f | |||
| 7c0468ca9d | |||
| 83eff4beec | |||
| 1f8f8c54fb | |||
| aa23448501 | |||
| d02ae12a19 |
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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 })),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user