Merge pull request #1192 from RooVetGit/cte/move-model-fetchers

Move & clean up model fetchers
This commit is contained in:
Chris Estreich 2025-02-25 13:26:39 -08:00 committed by GitHub
commit 17f0a5d961
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 603 additions and 620 deletions

View file

@ -1,10 +1,12 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import OpenAI from "openai"
import { ApiHandler, SingleCompletionHandler } from "../"
import { ApiHandlerOptions, ModelInfo, glamaDefaultModelId, glamaDefaultModelInfo } from "../../shared/api"
import { parseApiPrice } from "../../utils/cost"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { ApiHandler, SingleCompletionHandler } from "../"
const GLAMA_DEFAULT_TEMPERATURE = 0
@ -190,3 +192,44 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
}
}
}
export async function getGlamaModels() {
const models: Record<string, ModelInfo> = {}
try {
const response = await axios.get("https://glama.ai/api/gateway/v1/models")
const rawModels = response.data
for (const rawModel of rawModels) {
const modelInfo: ModelInfo = {
maxTokens: rawModel.maxTokensOutput,
contextWindow: rawModel.maxTokensInput,
supportsImages: rawModel.capabilities?.includes("input:image"),
supportsComputerUse: rawModel.capabilities?.includes("computer_use"),
supportsPromptCache: rawModel.capabilities?.includes("caching"),
inputPrice: parseApiPrice(rawModel.pricePerToken?.input),
outputPrice: parseApiPrice(rawModel.pricePerToken?.output),
description: undefined,
cacheWritesPrice: parseApiPrice(rawModel.pricePerToken?.cacheWrite),
cacheReadsPrice: parseApiPrice(rawModel.pricePerToken?.cacheRead),
}
switch (rawModel.id) {
case rawModel.id.startsWith("anthropic/claude-3-7-sonnet"):
modelInfo.maxTokens = 16384
break
case rawModel.id.startsWith("anthropic/"):
modelInfo.maxTokens = 8192
break
default:
break
}
models[rawModel.id] = modelInfo
}
} catch (error) {
console.error(`Error fetching Glama models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
}
return models
}

View file

@ -1,5 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import axios from "axios"
import { ApiHandler, SingleCompletionHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
@ -72,3 +74,17 @@ export class LmStudioHandler implements ApiHandler, SingleCompletionHandler {
}
}
}
export async function getLmStudioModels(baseUrl = "http://localhost:1234") {
try {
if (!URL.canParse(baseUrl)) {
return []
}
const response = await axios.get(`${baseUrl}/v1/models`)
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
return [...new Set<string>(modelsArray)]
} catch (error) {
return []
}
}

View file

@ -1,5 +1,7 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import axios from "axios"
import { ApiHandler, SingleCompletionHandler } from "../"
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
@ -88,3 +90,17 @@ export class OllamaHandler implements ApiHandler, SingleCompletionHandler {
}
}
}
export async function getOllamaModels(baseUrl = "http://localhost:11434") {
try {
if (!URL.canParse(baseUrl)) {
return []
}
const response = await axios.get(`${baseUrl}/api/tags`)
const modelsArray = response.data?.models?.map((model: any) => model.name) || []
return [...new Set<string>(modelsArray)]
} catch (error) {
return []
}
}

View file

@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI, { AzureOpenAI } from "openai"
import axios from "axios"
import {
ApiHandlerOptions,
@ -166,3 +167,27 @@ export class OpenAiHandler implements ApiHandler, SingleCompletionHandler {
}
}
}
export async function getOpenAiModels(baseUrl?: string, apiKey?: string) {
try {
if (!baseUrl) {
return []
}
if (!URL.canParse(baseUrl)) {
return []
}
const config: Record<string, any> = {}
if (apiKey) {
config["headers"] = { Authorization: `Bearer ${apiKey}` }
}
const response = await axios.get(`${baseUrl}/models`, config)
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
return [...new Set<string>(modelsArray)]
} catch (error) {
return []
}
}

View file

@ -1,29 +1,29 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import OpenAI from "openai"
import { ApiHandler } from "../"
import delay from "delay"
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
import { parseApiPrice } from "../../utils/cost"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStreamChunk, ApiStreamUsageChunk } from "../transform/stream"
import delay from "delay"
import { convertToR1Format } from "../transform/r1-format"
import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "./openai"
import { ApiHandler, SingleCompletionHandler } from ".."
const OPENROUTER_DEFAULT_TEMPERATURE = 0
// Add custom interface for OpenRouter params
// Add custom interface for OpenRouter params.
type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
transforms?: string[]
include_reasoning?: boolean
}
// Add custom interface for OpenRouter usage chunk
// Add custom interface for OpenRouter usage chunk.
interface OpenRouterApiStreamUsageChunk extends ApiStreamUsageChunk {
fullResponseText: string
}
import { SingleCompletionHandler } from ".."
import { convertToR1Format } from "../transform/r1-format"
export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
private options: ApiHandlerOptions
private client: OpenAI
@ -222,3 +222,75 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
}
}
}
export async function getOpenRouterModels() {
const models: Record<string, ModelInfo> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models")
const rawModels = response.data.data
for (const rawModel of rawModels) {
const modelInfo: ModelInfo = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
supportsImages: rawModel.architecture?.modality?.includes("image"),
supportsPromptCache: false,
inputPrice: parseApiPrice(rawModel.pricing?.prompt),
outputPrice: parseApiPrice(rawModel.pricing?.completion),
description: rawModel.description,
}
// NOTE: this needs to be synced with api.ts/openrouter default model info.
switch (true) {
case rawModel.id.startsWith("anthropic/claude-3.7-sonnet"):
modelInfo.supportsComputerUse = true
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
modelInfo.maxTokens = 16384
break
case rawModel.id.startsWith("anthropic/claude-3.5-sonnet-20240620"):
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
modelInfo.maxTokens = 8192
break
case rawModel.id.startsWith("anthropic/claude-3.5-sonnet"):
modelInfo.supportsComputerUse = true
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
modelInfo.maxTokens = 8192
break
case rawModel.id.startsWith("anthropic/claude-3-5-haiku"):
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 1.25
modelInfo.cacheReadsPrice = 0.1
modelInfo.maxTokens = 8192
break
case rawModel.id.startsWith("anthropic/claude-3-opus"):
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 18.75
modelInfo.cacheReadsPrice = 1.5
modelInfo.maxTokens = 8192
break
case rawModel.id.startsWith("anthropic/claude-3-haiku"):
default:
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 0.3
modelInfo.cacheReadsPrice = 0.03
modelInfo.maxTokens = 8192
break
}
models[rawModel.id] = modelInfo
}
} catch (error) {
console.error(
`Error fetching OpenRouter models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
)
}
return models
}

View file

@ -1,6 +1,9 @@
import { OpenAiHandler, OpenAiHandlerOptions } from "./openai"
import axios from "axios"
import { ModelInfo, requestyModelInfoSaneDefaults, requestyDefaultModelId } from "../../shared/api"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { parseApiPrice } from "../../utils/cost"
import { ApiStreamUsageChunk } from "../transform/stream"
import { OpenAiHandler, OpenAiHandlerOptions } from "./openai"
export class RequestyHandler extends OpenAiHandler {
constructor(options: OpenAiHandlerOptions) {
@ -38,3 +41,51 @@ export class RequestyHandler extends OpenAiHandler {
}
}
}
export async function getRequestyModels({ apiKey }: { apiKey?: string }) {
const models: Record<string, ModelInfo> = {}
if (!apiKey) {
return models
}
try {
const config: Record<string, any> = {}
config["headers"] = { Authorization: `Bearer ${apiKey}` }
const response = await axios.get("https://router.requesty.ai/v1/models", config)
const rawModels = response.data.data
for (const rawModel of rawModels) {
const modelInfo: ModelInfo = {
maxTokens: rawModel.max_output_tokens,
contextWindow: rawModel.context_window,
supportsImages: rawModel.support_image,
supportsComputerUse: rawModel.support_computer_use,
supportsPromptCache: rawModel.supports_caching,
inputPrice: parseApiPrice(rawModel.input_price),
outputPrice: parseApiPrice(rawModel.output_price),
description: rawModel.description,
cacheWritesPrice: parseApiPrice(rawModel.caching_price),
cacheReadsPrice: parseApiPrice(rawModel.cached_price),
}
switch (rawModel.id) {
case rawModel.id.startsWith("anthropic/claude-3-7-sonnet"):
modelInfo.maxTokens = 16384
break
case rawModel.id.startsWith("anthropic/"):
modelInfo.maxTokens = 8192
break
default:
break
}
models[rawModel.id] = modelInfo
}
} catch (error) {
console.error(`Error fetching Requesty models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
}
return models
}

View file

@ -1,9 +1,11 @@
import { Anthropic } from "@anthropic-ai/sdk"
import axios from "axios"
import OpenAI from "openai"
import { ApiHandler, SingleCompletionHandler } from "../"
import { ApiHandlerOptions, ModelInfo, unboundDefaultModelId, unboundDefaultModelInfo } from "../../shared/api"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { ApiHandler, SingleCompletionHandler } from "../"
interface UnboundUsage extends OpenAI.CompletionUsage {
cache_creation_input_tokens?: number
@ -163,3 +165,46 @@ export class UnboundHandler implements ApiHandler, SingleCompletionHandler {
}
}
}
export async function getUnboundModels() {
const models: Record<string, ModelInfo> = {}
try {
const response = await axios.get("https://api.getunbound.ai/models")
if (response.data) {
const rawModels: Record<string, any> = response.data
for (const [modelId, model] of Object.entries(rawModels)) {
const modelInfo: ModelInfo = {
maxTokens: model?.maxTokens ? parseInt(model.maxTokens) : undefined,
contextWindow: model?.contextWindow ? parseInt(model.contextWindow) : 0,
supportsImages: model?.supportsImages ?? false,
supportsPromptCache: model?.supportsPromptCaching ?? false,
supportsComputerUse: model?.supportsComputerUse ?? false,
inputPrice: model?.inputTokenPrice ? parseFloat(model.inputTokenPrice) : undefined,
outputPrice: model?.outputTokenPrice ? parseFloat(model.outputTokenPrice) : undefined,
cacheWritesPrice: model?.cacheWritePrice ? parseFloat(model.cacheWritePrice) : undefined,
cacheReadsPrice: model?.cacheReadPrice ? parseFloat(model.cacheReadPrice) : undefined,
}
switch (true) {
case modelId.startsWith("anthropic/claude-3-7-sonnet"):
modelInfo.maxTokens = 16384
break
case modelId.startsWith("anthropic/"):
modelInfo.maxTokens = 8192
break
default:
break
}
models[modelId] = modelInfo
}
}
} catch (error) {
console.error(`Error fetching Unbound models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
}
return models
}

View file

@ -1,5 +1,6 @@
import { Anthropic } from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import { ApiHandler, SingleCompletionHandler } from "../"
import { calculateApiCost } from "../../utils/cost"
import { ApiStream } from "../transform/stream"
@ -545,3 +546,15 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
}
}
}
export async function getVsCodeLmModels() {
try {
const models = await vscode.lm.selectChatModels({})
return models || []
} catch (error) {
console.error(
`Error fetching VS Code LM models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
)
return []
}
}

View file

@ -47,6 +47,8 @@ import {
import { getApiMetrics } from "../shared/getApiMetrics"
import { HistoryItem } from "../shared/HistoryItem"
import { ClineAskResponse } from "../shared/WebviewMessage"
import { GlobalFileNames } from "../shared/globalFileNames"
import { defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes"
import { calculateApiCost } from "../utils/cost"
import { fileExistsAtPath } from "../utils/fs"
import { arePathsEqual, getReadablePath } from "../utils/path"
@ -54,12 +56,10 @@ import { parseMentions } from "./mentions"
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
import { formatResponse } from "./prompts/responses"
import { SYSTEM_PROMPT } from "./prompts/system"
import { modes, defaultModeSlug, getModeBySlug, getFullModeDetails } from "../shared/modes"
import { truncateConversationIfNeeded } from "./sliding-window"
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
import { ClineProvider } from "./webview/ClineProvider"
import { detectCodeOmission } from "../integrations/editor/detect-omission"
import { BrowserSession } from "../services/browser/BrowserSession"
import { OpenRouterHandler } from "../api/providers/openrouter"
import { McpHub } from "../services/mcp/McpHub"
import crypto from "crypto"
import { insertGroups } from "./diff/insert-groups"

View file

@ -1,3 +1,5 @@
// npx jest src/core/config/__tests__/CustomModesManager.test.ts
import * as vscode from "vscode"
import * as path from "path"
import * as fs from "fs/promises"
@ -15,9 +17,10 @@ describe("CustomModesManager", () => {
let mockOnUpdate: jest.Mock
let mockWorkspaceFolders: { uri: { fsPath: string } }[]
const mockStoragePath = "/mock/settings"
// Use path.sep to ensure correct path separators for the current platform
const mockStoragePath = `${path.sep}mock${path.sep}settings`
const mockSettingsPath = path.join(mockStoragePath, "settings", "cline_custom_modes.json")
const mockRoomodes = "/mock/workspace/.roomodes"
const mockRoomodes = `${path.sep}mock${path.sep}workspace${path.sep}.roomodes`
beforeEach(() => {
mockOnUpdate = jest.fn()
@ -243,7 +246,15 @@ describe("CustomModesManager", () => {
await manager.updateCustomMode("project-mode", projectMode)
// Verify .roomodes was created with the project mode
expect(fs.writeFile).toHaveBeenCalledWith(mockRoomodes, expect.stringContaining("project-mode"), "utf-8")
expect(fs.writeFile).toHaveBeenCalledWith(
expect.any(String), // Don't check exact path as it may have different separators on different platforms
expect.stringContaining("project-mode"),
"utf-8",
)
// Verify the path is correct regardless of separators
const writeCall = (fs.writeFile as jest.Mock).mock.calls[0]
expect(path.normalize(writeCall[0])).toBe(path.normalize(mockRoomodes))
// Verify the content written to .roomodes
expect(roomodesContent).toEqual({

View file

@ -8,138 +8,51 @@ import * as path from "path"
import * as vscode from "vscode"
import simpleGit from "simple-git"
import { buildApiHandler } from "../../api"
import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt"
import { GlobalFileNames } from "../../shared/globalFileNames"
import type { SecretKey, GlobalStateKey } from "../../shared/globalState"
import { HistoryItem } from "../../shared/HistoryItem"
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug } from "../../shared/modes"
import { checkExistKey } from "../../shared/checkExistApiConfig"
import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
import { downloadTask } from "../../integrations/misc/export-markdown"
import { openFile, openImage } from "../../integrations/misc/open-file"
import { selectImages } from "../../integrations/misc/process-images"
import { getTheme } from "../../integrations/theme/getTheme"
import { getDiffStrategy } from "../diff/DiffStrategy"
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
import { McpHub } from "../../services/mcp/McpHub"
import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
import { findLast } from "../../shared/array"
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
import { HistoryItem } from "../../shared/HistoryItem"
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug } from "../../shared/modes"
import { SYSTEM_PROMPT } from "../prompts/system"
import { McpServerManager } from "../../services/mcp/McpServerManager"
import { fileExistsAtPath } from "../../utils/fs"
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
import { singleCompletionHandler } from "../../utils/single-completion-handler"
import { searchCommits } from "../../utils/git"
import { getDiffStrategy } from "../diff/DiffStrategy"
import { SYSTEM_PROMPT } from "../prompts/system"
import { ConfigManager } from "../config/ConfigManager"
import { CustomModesManager } from "../config/CustomModesManager"
import { buildApiHandler } from "../../api"
import { getOpenRouterModels } from "../../api/providers/openrouter"
import { getGlamaModels } from "../../api/providers/glama"
import { getUnboundModels } from "../../api/providers/unbound"
import { getRequestyModels } from "../../api/providers/requesty"
import { getOpenAiModels } from "../../api/providers/openai"
import { getOllamaModels } from "../../api/providers/ollama"
import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
import { getLmStudioModels } from "../../api/providers/lmstudio"
import { ACTION_NAMES } from "../CodeActionProvider"
import { Cline } from "../Cline"
import { openMention } from "../mentions"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
import { checkExistKey } from "../../shared/checkExistApiConfig"
import { singleCompletionHandler } from "../../utils/single-completion-handler"
import { searchCommits } from "../../utils/git"
import { ConfigManager } from "../config/ConfigManager"
import { CustomModesManager } from "../config/CustomModesManager"
import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt"
import { ACTION_NAMES } from "../CodeActionProvider"
import { McpServerManager } from "../../services/mcp/McpServerManager"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
*/
type SecretKey =
| "apiKey"
| "glamaApiKey"
| "openRouterApiKey"
| "awsAccessKey"
| "awsSecretKey"
| "awsSessionToken"
| "openAiApiKey"
| "geminiApiKey"
| "openAiNativeApiKey"
| "deepSeekApiKey"
| "mistralApiKey"
| "unboundApiKey"
| "requestyApiKey"
type GlobalStateKey =
| "apiProvider"
| "apiModelId"
| "glamaModelId"
| "glamaModelInfo"
| "awsRegion"
| "awsUseCrossRegionInference"
| "awsProfile"
| "awsUseProfile"
| "vertexProjectId"
| "vertexRegion"
| "lastShownAnnouncementId"
| "customInstructions"
| "alwaysAllowReadOnly"
| "alwaysAllowWrite"
| "alwaysAllowExecute"
| "alwaysAllowBrowser"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
| "taskHistory"
| "openAiBaseUrl"
| "openAiModelId"
| "openAiCustomModelInfo"
| "openAiUseAzure"
| "ollamaModelId"
| "ollamaBaseUrl"
| "lmStudioModelId"
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
| "openRouterModelInfo"
| "openRouterBaseUrl"
| "openRouterUseMiddleOutTransform"
| "allowedCommands"
| "soundEnabled"
| "soundVolume"
| "diffEnabled"
| "checkpointsEnabled"
| "browserViewportSize"
| "screenshotQuality"
| "fuzzyMatchThreshold"
| "preferredLanguage" // Language setting for Cline's communication
| "writeDelayMs"
| "terminalOutputLineLimit"
| "mcpEnabled"
| "enableMcpServerCreation"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "rateLimitSeconds"
| "currentApiConfigName"
| "listApiConfigMeta"
| "vsCodeLmModelSelector"
| "mode"
| "modeApiConfigs"
| "customModePrompts"
| "customSupportPrompts"
| "enhancementApiConfigId"
| "experiments" // Map of experiment IDs to their enabled state
| "autoApprovalEnabled"
| "customModes" // Array of custom modes
| "unboundModelId"
| "requestyModelId"
| "requestyModelInfo"
| "unboundModelInfo"
| "modelTemperature"
| "mistralCodestralUrl"
| "maxOpenTabsContext"
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
uiMessages: "ui_messages.json",
glamaModels: "glama_models.json",
openRouterModels: "openrouter_models.json",
requestyModels: "requesty_models.json",
mcpSettings: "cline_mcp_settings.json",
unboundModels: "unbound_models.json",
}
/**
* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
* https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
*/
export class ClineProvider implements vscode.WebviewViewProvider {
public static readonly sideBarId = "roo-cline.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
@ -619,15 +532,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.postStateToWebview()
this.workspaceTracker?.initializeFilePaths() // don't await
getTheme().then((theme) =>
this.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }),
)
// post last cached models in case the call to endpoint fails
this.readOpenRouterModels().then((openRouterModels) => {
if (openRouterModels) {
this.postMessageToWebview({ type: "openRouterModels", openRouterModels })
}
})
// If MCP Hub is already initialized, update the webview with current server list
if (this.mcpHub) {
@ -637,13 +545,37 @@ export class ClineProvider implements vscode.WebviewViewProvider {
})
}
// gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch.
// we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point
// (see normalizeApiConfiguration > openrouter)
this.refreshOpenRouterModels().then(async (openRouterModels) => {
const cacheDir = await this.ensureCacheDirectoryExists()
// Post last cached models in case the call to endpoint fails.
this.readModelsFromCache(GlobalFileNames.openRouterModels).then((openRouterModels) => {
if (openRouterModels) {
// 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)
this.postMessageToWebview({ type: "openRouterModels", openRouterModels })
}
})
// GUI relies on model info to be up-to-date to provide
// the most accurate pricing, so we need to fetch the
// latest details on launch.
// We do this for all users since many users switch
// between api providers and if they were to switch back
// to OpenRouter it would be showing outdated model info
// if we hadn't retrieved the latest at this point
// (see normalizeApiConfiguration > openrouter).
getOpenRouterModels().then(async (openRouterModels) => {
if (Object.keys(openRouterModels).length > 0) {
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.openRouterModels),
JSON.stringify(openRouterModels),
)
await this.postMessageToWebview({ type: "openRouterModels", openRouterModels })
// 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 } = await this.getState()
if (apiConfiguration.openRouterModelId) {
await this.updateGlobalState(
"openRouterModelInfo",
@ -653,15 +585,23 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
})
this.readGlamaModels().then((glamaModels) => {
this.readModelsFromCache(GlobalFileNames.glamaModels).then((glamaModels) => {
if (glamaModels) {
this.postMessageToWebview({ type: "glamaModels", glamaModels })
}
})
this.refreshGlamaModels().then(async (glamaModels) => {
if (glamaModels) {
// 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)
getGlamaModels().then(async (glamaModels) => {
if (Object.keys(glamaModels).length > 0) {
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.glamaModels),
JSON.stringify(glamaModels),
)
await this.postMessageToWebview({ type: "glamaModels", glamaModels })
const { apiConfiguration } = await this.getState()
if (apiConfiguration.glamaModelId) {
await this.updateGlobalState(
"glamaModelInfo",
@ -672,14 +612,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
})
this.readUnboundModels().then((unboundModels) => {
this.readModelsFromCache(GlobalFileNames.unboundModels).then((unboundModels) => {
if (unboundModels) {
this.postMessageToWebview({ type: "unboundModels", unboundModels })
}
})
this.refreshUnboundModels().then(async (unboundModels) => {
if (unboundModels) {
getUnboundModels().then(async (unboundModels) => {
if (Object.keys(unboundModels).length > 0) {
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.unboundModels),
JSON.stringify(unboundModels),
)
await this.postMessageToWebview({ type: "unboundModels", unboundModels })
const { apiConfiguration } = await this.getState()
if (apiConfiguration?.unboundModelId) {
await this.updateGlobalState(
"unboundModelInfo",
@ -690,15 +638,24 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
})
this.readRequestyModels().then((requestyModels) => {
this.readModelsFromCache(GlobalFileNames.requestyModels).then((requestyModels) => {
if (requestyModels) {
this.postMessageToWebview({ type: "requestyModels", requestyModels })
}
})
this.refreshRequestyModels().then(async (requestyModels) => {
if (requestyModels) {
// 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 requestyApiKey = await this.getSecret("requestyApiKey")
getRequestyModels({ apiKey: requestyApiKey }).then(async (requestyModels) => {
if (Object.keys(requestyModels).length > 0) {
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.requestyModels),
JSON.stringify(requestyModels),
)
await this.postMessageToWebview({ type: "requestyModels", requestyModels })
const { apiConfiguration } = await this.getState()
if (apiConfiguration.requestyModelId) {
await this.updateGlobalState(
"requestyModelInfo",
@ -841,41 +798,84 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "resetState":
await this.resetState()
break
case "requestOllamaModels":
const ollamaModels = await this.getOllamaModels(message.text)
this.postMessageToWebview({ type: "ollamaModels", ollamaModels })
break
case "requestLmStudioModels":
const lmStudioModels = await this.getLmStudioModels(message.text)
this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels })
break
case "requestVsCodeLmModels":
const vsCodeLmModels = await this.getVsCodeLmModels()
this.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
case "refreshOpenRouterModels":
const openRouterModels = await getOpenRouterModels()
if (Object.keys(openRouterModels).length > 0) {
const cacheDir = await this.ensureCacheDirectoryExists()
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.openRouterModels),
JSON.stringify(openRouterModels),
)
await this.postMessageToWebview({ type: "openRouterModels", openRouterModels })
}
break
case "refreshGlamaModels":
await this.refreshGlamaModels()
const glamaModels = await getGlamaModels()
if (Object.keys(glamaModels).length > 0) {
const cacheDir = await this.ensureCacheDirectoryExists()
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.glamaModels),
JSON.stringify(glamaModels),
)
await this.postMessageToWebview({ type: "glamaModels", glamaModels })
}
break
case "refreshOpenRouterModels":
await this.refreshOpenRouterModels()
case "refreshUnboundModels":
const unboundModels = await getUnboundModels()
if (Object.keys(unboundModels).length > 0) {
const cacheDir = await this.ensureCacheDirectoryExists()
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.unboundModels),
JSON.stringify(unboundModels),
)
await this.postMessageToWebview({ type: "unboundModels", unboundModels })
}
break
case "refreshRequestyModels":
if (message?.values?.apiKey) {
const requestyModels = await getRequestyModels({ apiKey: message.values.apiKey })
if (Object.keys(requestyModels).length > 0) {
const cacheDir = await this.ensureCacheDirectoryExists()
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.requestyModels),
JSON.stringify(requestyModels),
)
await this.postMessageToWebview({ type: "requestyModels", requestyModels })
}
}
break
case "refreshOpenAiModels":
if (message?.values?.baseUrl && message?.values?.apiKey) {
const openAiModels = await this.getOpenAiModels(
const openAiModels = await getOpenAiModels(
message?.values?.baseUrl,
message?.values?.apiKey,
)
this.postMessageToWebview({ type: "openAiModels", openAiModels })
}
break
case "refreshUnboundModels":
await this.refreshUnboundModels()
case "requestOllamaModels":
const ollamaModels = await getOllamaModels(message.text)
// TODO: Cache like we do for OpenRouter, etc?
this.postMessageToWebview({ type: "ollamaModels", ollamaModels })
break
case "refreshRequestyModels":
if (message?.values?.apiKey) {
const requestyModels = await this.refreshRequestyModels(message?.values?.apiKey)
this.postMessageToWebview({ type: "requestyModels", requestyModels: requestyModels })
}
case "requestLmStudioModels":
const lmStudioModels = await getLmStudioModels(message.text)
// TODO: Cache like we do for OpenRouter, etc?
this.postMessageToWebview({ type: "lmStudioModels", lmStudioModels })
break
case "requestVsCodeLmModels":
const vsCodeLmModels = await getVsCodeLmModels()
// TODO: Cache like we do for OpenRouter, etc?
this.postMessageToWebview({ type: "vsCodeLmModels", vsCodeLmModels })
break
case "openImage":
openImage(message.text!)
@ -1789,170 +1789,24 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return settingsDir
}
// Ollama
async getOllamaModels(baseUrl?: string) {
try {
if (!baseUrl) {
baseUrl = "http://localhost:11434"
}
if (!URL.canParse(baseUrl)) {
return []
}
const response = await axios.get(`${baseUrl}/api/tags`)
const modelsArray = response.data?.models?.map((model: any) => model.name) || []
const models = [...new Set<string>(modelsArray)]
return models
} catch (error) {
return []
}
private async ensureCacheDirectoryExists() {
const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
// LM Studio
private async readModelsFromCache(filename: string): Promise<Record<string, ModelInfo> | undefined> {
const filePath = path.join(await this.ensureCacheDirectoryExists(), filename)
const fileExists = await fileExistsAtPath(filePath)
async getLmStudioModels(baseUrl?: string) {
try {
if (!baseUrl) {
baseUrl = "http://localhost:1234"
}
if (!URL.canParse(baseUrl)) {
return []
}
const response = await axios.get(`${baseUrl}/v1/models`)
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
const models = [...new Set<string>(modelsArray)]
return models
} catch (error) {
return []
}
}
// VSCode LM API
private async getVsCodeLmModels() {
try {
const models = await vscode.lm.selectChatModels({})
return models || []
} catch (error) {
this.outputChannel.appendLine(
`Error fetching VS Code LM models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
)
return []
}
}
// OpenAi
async getOpenAiModels(baseUrl?: string, apiKey?: string) {
try {
if (!baseUrl) {
return []
}
if (!URL.canParse(baseUrl)) {
return []
}
const config: Record<string, any> = {}
if (apiKey) {
config["headers"] = { Authorization: `Bearer ${apiKey}` }
}
const response = await axios.get(`${baseUrl}/models`, config)
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
const models = [...new Set<string>(modelsArray)]
return models
} catch (error) {
return []
}
}
// Requesty
async readRequestyModels(): Promise<Record<string, ModelInfo> | undefined> {
const requestyModelsFilePath = path.join(
await this.ensureCacheDirectoryExists(),
GlobalFileNames.requestyModels,
)
const fileExists = await fileExistsAtPath(requestyModelsFilePath)
if (fileExists) {
const fileContents = await fs.readFile(requestyModelsFilePath, "utf8")
const fileContents = await fs.readFile(filePath, "utf8")
return JSON.parse(fileContents)
}
return undefined
}
async refreshRequestyModels(apiKey?: string) {
const requestyModelsFilePath = path.join(
await this.ensureCacheDirectoryExists(),
GlobalFileNames.requestyModels,
)
const models: Record<string, ModelInfo> = {}
try {
const config: Record<string, any> = {}
if (!apiKey) {
apiKey = (await this.getSecret("requestyApiKey")) as string
}
if (!apiKey) {
this.outputChannel.appendLine("No Requesty API key found")
return models
}
if (apiKey) {
config["headers"] = { Authorization: `Bearer ${apiKey}` }
}
const response = await axios.get("https://router.requesty.ai/v1/models", config)
if (response.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) {
const modelInfo: ModelInfo = {
maxTokens: rawModel.max_output_tokens,
contextWindow: rawModel.context_window,
supportsImages: rawModel.support_image,
supportsComputerUse: rawModel.support_computer_use,
supportsPromptCache: rawModel.supports_caching,
inputPrice: parsePrice(rawModel.input_price),
outputPrice: parsePrice(rawModel.output_price),
description: rawModel.description,
cacheWritesPrice: parsePrice(rawModel.caching_price),
cacheReadsPrice: parsePrice(rawModel.cached_price),
}
switch (rawModel.id) {
case rawModel.id.startsWith("anthropic/claude-3-7-sonnet"):
modelInfo.maxTokens = 16384
break
case rawModel.id.startsWith("anthropic/"):
modelInfo.maxTokens = 8192
break
default:
break
}
models[rawModel.id] = modelInfo
}
} else {
this.outputChannel.appendLine("Invalid response from Requesty API")
}
await fs.writeFile(requestyModelsFilePath, JSON.stringify(models))
} catch (error) {
this.outputChannel.appendLine(
`Error fetching Requesty models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
)
}
await this.postMessageToWebview({ type: "requestyModels", requestyModels: models })
return models
}
// OpenRouter
async handleOpenRouterCallback(code: string) {
@ -1981,11 +1835,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
}
private async ensureCacheDirectoryExists(): Promise<string> {
const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
await fs.mkdir(cacheDir, { recursive: true })
return cacheDir
}
// Glama
async handleGlamaCallback(code: string) {
let apiKey: string
@ -2016,249 +1866,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
}
private async readModelsFromCache(filename: string): Promise<Record<string, ModelInfo> | undefined> {
const filePath = path.join(await this.ensureCacheDirectoryExists(), filename)
const fileExists = await fileExistsAtPath(filePath)
if (fileExists) {
const fileContents = await fs.readFile(filePath, "utf8")
return JSON.parse(fileContents)
}
return undefined
}
async readGlamaModels(): Promise<Record<string, ModelInfo> | undefined> {
return this.readModelsFromCache(GlobalFileNames.glamaModels)
}
async refreshGlamaModels() {
const glamaModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.glamaModels)
const models: Record<string, ModelInfo> = {}
try {
const response = await axios.get("https://glama.ai/api/gateway/v1/models")
/*
{
"added": "2024-12-24T15:12:49.324Z",
"capabilities": [
"adjustable_safety_settings",
"caching",
"code_execution",
"function_calling",
"json_mode",
"json_schema",
"system_instructions",
"tuning",
"input:audio",
"input:image",
"input:text",
"input:video",
"output:text"
],
"id": "google-vertex/gemini-1.5-flash-002",
"maxTokensInput": 1048576,
"maxTokensOutput": 8192,
"pricePerToken": {
"cacheRead": null,
"cacheWrite": null,
"input": "0.000000075",
"output": "0.0000003"
}
}
*/
if (response.data) {
const rawModels = response.data
const parsePrice = (price: any) => {
if (price) {
return parseFloat(price) * 1_000_000
}
return undefined
}
for (const rawModel of rawModels) {
const modelInfo: ModelInfo = {
maxTokens: rawModel.maxTokensOutput,
contextWindow: rawModel.maxTokensInput,
supportsImages: rawModel.capabilities?.includes("input:image"),
supportsComputerUse: rawModel.capabilities?.includes("computer_use"),
supportsPromptCache: rawModel.capabilities?.includes("caching"),
inputPrice: parsePrice(rawModel.pricePerToken?.input),
outputPrice: parsePrice(rawModel.pricePerToken?.output),
description: undefined,
cacheWritesPrice: parsePrice(rawModel.pricePerToken?.cacheWrite),
cacheReadsPrice: parsePrice(rawModel.pricePerToken?.cacheRead),
}
switch (rawModel.id) {
case rawModel.id.startsWith("anthropic/claude-3-7-sonnet"):
modelInfo.maxTokens = 16384
break
case rawModel.id.startsWith("anthropic/"):
modelInfo.maxTokens = 8192
break
default:
break
}
models[rawModel.id] = modelInfo
}
} else {
this.outputChannel.appendLine("Invalid response from Glama API")
}
await fs.writeFile(glamaModelsFilePath, JSON.stringify(models))
} catch (error) {
this.outputChannel.appendLine(
`Error fetching Glama models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
)
}
await this.postMessageToWebview({ type: "glamaModels", glamaModels: models })
return models
}
async readOpenRouterModels(): Promise<Record<string, ModelInfo> | undefined> {
return this.readModelsFromCache(GlobalFileNames.openRouterModels)
}
async refreshOpenRouterModels() {
const openRouterModelsFilePath = path.join(
await this.ensureCacheDirectoryExists(),
GlobalFileNames.openRouterModels,
)
const models: Record<string, ModelInfo> = {}
try {
const response = await axios.get("https://openrouter.ai/api/v1/models")
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) {
const modelInfo: ModelInfo = {
maxTokens: rawModel.top_provider?.max_completion_tokens,
contextWindow: rawModel.context_length,
supportsImages: rawModel.architecture?.modality?.includes("image"),
supportsPromptCache: false,
inputPrice: parsePrice(rawModel.pricing?.prompt),
outputPrice: parsePrice(rawModel.pricing?.completion),
description: rawModel.description,
}
// NOTE: this needs to be synced with api.ts/openrouter default model info.
switch (true) {
case rawModel.id.startsWith("anthropic/claude-3.7-sonnet"):
modelInfo.supportsComputerUse = true
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
modelInfo.maxTokens = 16384
break
case rawModel.id.startsWith("anthropic/claude-3.5-sonnet-20240620"):
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
modelInfo.maxTokens = 8192
break
case rawModel.id.startsWith("anthropic/claude-3.5-sonnet"):
modelInfo.supportsComputerUse = true
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 3.75
modelInfo.cacheReadsPrice = 0.3
modelInfo.maxTokens = 8192
break
case rawModel.id.startsWith("anthropic/claude-3-5-haiku"):
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 1.25
modelInfo.cacheReadsPrice = 0.1
modelInfo.maxTokens = 8192
break
case rawModel.id.startsWith("anthropic/claude-3-opus"):
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 18.75
modelInfo.cacheReadsPrice = 1.5
modelInfo.maxTokens = 8192
break
case rawModel.id.startsWith("anthropic/claude-3-haiku"):
default:
modelInfo.supportsPromptCache = true
modelInfo.cacheWritesPrice = 0.3
modelInfo.cacheReadsPrice = 0.03
modelInfo.maxTokens = 8192
break
}
models[rawModel.id] = modelInfo
}
} else {
this.outputChannel.appendLine("Invalid response from OpenRouter API")
}
await fs.writeFile(openRouterModelsFilePath, JSON.stringify(models))
} catch (error) {
this.outputChannel.appendLine(
`Error fetching OpenRouter models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
)
}
await this.postMessageToWebview({ type: "openRouterModels", openRouterModels: models })
return models
}
async readUnboundModels(): Promise<Record<string, ModelInfo> | undefined> {
return this.readModelsFromCache(GlobalFileNames.unboundModels)
}
async refreshUnboundModels() {
const unboundModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.unboundModels)
const models: Record<string, ModelInfo> = {}
try {
const response = await axios.get("https://api.getunbound.ai/models")
if (response.data) {
const rawModels: Record<string, any> = response.data
for (const [modelId, model] of Object.entries(rawModels)) {
const modelInfo: ModelInfo = {
maxTokens: model?.maxTokens ? parseInt(model.maxTokens) : undefined,
contextWindow: model?.contextWindow ? parseInt(model.contextWindow) : 0,
supportsImages: model?.supportsImages ?? false,
supportsPromptCache: model?.supportsPromptCaching ?? false,
supportsComputerUse: model?.supportsComputerUse ?? false,
inputPrice: model?.inputTokenPrice ? parseFloat(model.inputTokenPrice) : undefined,
outputPrice: model?.outputTokenPrice ? parseFloat(model.outputTokenPrice) : undefined,
cacheWritesPrice: model?.cacheWritePrice ? parseFloat(model.cacheWritePrice) : undefined,
cacheReadsPrice: model?.cacheReadPrice ? parseFloat(model.cacheReadPrice) : undefined,
}
switch (true) {
case modelId.startsWith("anthropic/claude-3-7-sonnet"):
modelInfo.maxTokens = 16384
break
case modelId.startsWith("anthropic/"):
modelInfo.maxTokens = 8192
break
default:
break
}
models[modelId] = modelInfo
}
}
await fs.writeFile(unboundModelsFilePath, JSON.stringify(models))
} catch (error) {
this.outputChannel.appendLine(
`Error fetching Unbound models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
)
}
await this.postMessageToWebview({ type: "unboundModels", unboundModels: models })
return models
}
// Task history
async getTaskWithId(id: string): Promise<{
@ -2848,26 +2455,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return await this.context.globalState.get(key)
}
// workspace
private async updateWorkspaceState(key: string, value: any) {
await this.context.workspaceState.update(key, value)
}
private async getWorkspaceState(key: string) {
return await this.context.workspaceState.get(key)
}
// private async clearState() {
// this.context.workspaceState.keys().forEach((key) => {
// this.context.workspaceState.update(key, undefined)
// })
// this.context.globalState.keys().forEach((key) => {
// this.context.globalState.update(key, undefined)
// })
// this.context.secrets.delete("apiKey")
// }
// secrets
public async storeSecret(key: SecretKey, value?: string) {

View file

@ -14,7 +14,9 @@ import * as fs from "fs/promises"
import * as path from "path"
import * as vscode from "vscode"
import { z } from "zod"
import { ClineProvider, GlobalFileNames } from "../../core/webview/ClineProvider"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { GlobalFileNames } from "../../shared/globalFileNames"
import {
McpResource,
McpResourceResponse,

View file

@ -0,0 +1,9 @@
export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
uiMessages: "ui_messages.json",
glamaModels: "glama_models.json",
openRouterModels: "openrouter_models.json",
requestyModels: "requesty_models.json",
mcpSettings: "cline_mcp_settings.json",
unboundModels: "unbound_models.json",
}

85
src/shared/globalState.ts Normal file
View file

@ -0,0 +1,85 @@
export type SecretKey =
| "apiKey"
| "glamaApiKey"
| "openRouterApiKey"
| "awsAccessKey"
| "awsSecretKey"
| "awsSessionToken"
| "openAiApiKey"
| "geminiApiKey"
| "openAiNativeApiKey"
| "deepSeekApiKey"
| "mistralApiKey"
| "unboundApiKey"
| "requestyApiKey"
export type GlobalStateKey =
| "apiProvider"
| "apiModelId"
| "glamaModelId"
| "glamaModelInfo"
| "awsRegion"
| "awsUseCrossRegionInference"
| "awsProfile"
| "awsUseProfile"
| "vertexProjectId"
| "vertexRegion"
| "lastShownAnnouncementId"
| "customInstructions"
| "alwaysAllowReadOnly"
| "alwaysAllowWrite"
| "alwaysAllowExecute"
| "alwaysAllowBrowser"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
| "taskHistory"
| "openAiBaseUrl"
| "openAiModelId"
| "openAiCustomModelInfo"
| "openAiUseAzure"
| "ollamaModelId"
| "ollamaBaseUrl"
| "lmStudioModelId"
| "lmStudioBaseUrl"
| "anthropicBaseUrl"
| "anthropicThinking"
| "azureApiVersion"
| "openAiStreamingEnabled"
| "openRouterModelId"
| "openRouterModelInfo"
| "openRouterBaseUrl"
| "openRouterUseMiddleOutTransform"
| "allowedCommands"
| "soundEnabled"
| "soundVolume"
| "diffEnabled"
| "checkpointsEnabled"
| "browserViewportSize"
| "screenshotQuality"
| "fuzzyMatchThreshold"
| "preferredLanguage" // Language setting for Cline's communication
| "writeDelayMs"
| "terminalOutputLineLimit"
| "mcpEnabled"
| "enableMcpServerCreation"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "rateLimitSeconds"
| "currentApiConfigName"
| "listApiConfigMeta"
| "vsCodeLmModelSelector"
| "mode"
| "modeApiConfigs"
| "customModePrompts"
| "customSupportPrompts"
| "enhancementApiConfigId"
| "experiments" // Map of experiment IDs to their enabled state
| "autoApprovalEnabled"
| "customModes" // Array of custom modes
| "unboundModelId"
| "requestyModelId"
| "requestyModelInfo"
| "unboundModelInfo"
| "modelTemperature"
| "mistralCodestralUrl"
| "maxOpenTabsContext"

View file

@ -1,6 +1,9 @@
import { arePathsEqual, getReadablePath } from "../path"
import * as path from "path"
// npx jest src/utils/__tests__/path.test.ts
import os from "os"
import * as path from "path"
import { arePathsEqual, getReadablePath } from "../path"
describe("Path Utilities", () => {
const originalPlatform = process.platform
@ -92,22 +95,24 @@ describe("Path Utilities", () => {
describe("getReadablePath", () => {
const homeDir = os.homedir()
const desktop = path.join(homeDir, "Desktop")
const cwd = process.platform === "win32" ? "C:\\Users\\test\\project" : "/Users/test/project"
it("should return basename when path equals cwd", () => {
const cwd = "/Users/test/project"
expect(getReadablePath(cwd, cwd)).toBe("project")
})
it("should return relative path when inside cwd", () => {
const cwd = "/Users/test/project"
const filePath = "/Users/test/project/src/file.txt"
const filePath =
process.platform === "win32"
? "C:\\Users\\test\\project\\src\\file.txt"
: "/Users/test/project/src/file.txt"
expect(getReadablePath(cwd, filePath)).toBe("src/file.txt")
})
it("should return absolute path when outside cwd", () => {
const cwd = "/Users/test/project"
const filePath = "/Users/test/other/file.txt"
expect(getReadablePath(cwd, filePath)).toBe("/Users/test/other/file.txt")
const filePath =
process.platform === "win32" ? "C:\\Users\\test\\other\\file.txt" : "/Users/test/other/file.txt"
expect(getReadablePath(cwd, filePath)).toBe(filePath.toPosix())
})
it("should handle Desktop as cwd", () => {
@ -116,19 +121,20 @@ describe("Path Utilities", () => {
})
it("should handle undefined relative path", () => {
const cwd = "/Users/test/project"
expect(getReadablePath(cwd)).toBe("project")
})
it("should handle parent directory traversal", () => {
const cwd = "/Users/test/project"
const filePath = "../../other/file.txt"
expect(getReadablePath(cwd, filePath)).toBe("/Users/other/file.txt")
const filePath =
process.platform === "win32" ? "C:\\Users\\test\\other\\file.txt" : "/Users/test/other/file.txt"
expect(getReadablePath(cwd, filePath)).toBe(filePath.toPosix())
})
it("should normalize paths with redundant segments", () => {
const cwd = "/Users/test/project"
const filePath = "/Users/test/project/./src/../src/file.txt"
const filePath =
process.platform === "win32"
? "C:\\Users\\test\\project\\src\\file.txt"
: "/Users/test/project/./src/../src/file.txt"
expect(getReadablePath(cwd, filePath)).toBe("src/file.txt")
})
})

View file

@ -22,3 +22,5 @@ export function calculateApiCost(
const totalCost = cacheWritesCost + cacheReadsCost + baseInputCost + outputCost
return totalCost
}
export const parseApiPrice = (price: any) => (price ? parseFloat(price) * 1_000_000 : undefined)

View file

@ -351,7 +351,7 @@ const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
<Button variant="ghost" size="sm" onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
<span className="codicon codicon-cloud-download" />
</Button>
{item?.size && (
{!!item?.size && item.size > 0 && (
<Button
variant="ghost"
size="sm"