diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bf95687..eb61061 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -15,6 +15,7 @@ body: - AWS Bedrock - OpenAI - OpenAI Compatible + - LM Studio - Ollama validations: required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a83f1..23d91dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## [2.1.6] + +- Add LM Studio as an API provider option (make sure to start the LM Studio server to use it with the extension!) + +## [2.1.5] + +- Add support for prompt caching for new Claude model IDs on OpenRouter (e.g. `anthropic/claude-3.5-sonnet-20240620`) + ## [2.1.4] - AWS Bedrock fixes (add missing regions, support for cross-region inference, and older Sonnet model for regions where new model is not available) diff --git a/README.md b/README.md index 227847f..32f9140 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Thanks to [Claude 3.5 Sonnet's agentic coding capabilities](https://www-cdn.ant ### Use any API and Model -Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, and GCP Vertex. You can also configure any OpenAI compatible API, or use a local model through Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available. +Cline supports API providers like OpenRouter, Anthropic, OpenAI, Google Gemini, AWS Bedrock, Azure, and GCP Vertex. You can also configure any OpenAI compatible API, or use a local model through LM Studio/Ollama. If you're using OpenRouter, the extension fetches their latest model list, allowing you to use the newest models as soon as they're available. The extension also keeps track of total tokens and API usage cost for the entire task loop and individual requests, keeping you informed of spend every step of the way. @@ -138,7 +138,7 @@ To contribute to the project, start by exploring [open issues](https://github.co
Local Development Instructions -1. Clone the repository: +1. Clone the repository _(Requires [git-lfs](https://git-lfs.com/))_: ```bash git clone https://github.com/cline/cline.git ``` diff --git a/bin/roo-cline-2.0.1.vsix b/bin/roo-cline-2.0.1.vsix new file mode 100644 index 0000000..90f2132 Binary files /dev/null and b/bin/roo-cline-2.0.1.vsix differ diff --git a/package-lock.json b/package-lock.json index 91353c8..d19bbb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "roo-cline", - "version": "1.0.5", + "version": "2.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "roo-cline", - "version": "1.0.5", + "version": "2.0.1", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/sdk": "^0.26.0", diff --git a/package.json b/package.json index 236af73..0421765 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "roo-cline", "displayName": "Roo Cline", "description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.", - "version": "2.0.0", + "version": "2.0.1", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", diff --git a/src/api/index.ts b/src/api/index.ts index 388b9ce..ec35c2a 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -6,6 +6,7 @@ import { OpenRouterHandler } from "./providers/openrouter" import { VertexHandler } from "./providers/vertex" import { OpenAiHandler } from "./providers/openai" import { OllamaHandler } from "./providers/ollama" +import { LmStudioHandler } from "./providers/lmstudio" import { GeminiHandler } from "./providers/gemini" import { OpenAiNativeHandler } from "./providers/openai-native" import { ApiStream } from "./transform/stream" @@ -30,6 +31,8 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler { return new OpenAiHandler(options) case "ollama": return new OllamaHandler(options) + case "lmstudio": + return new LmStudioHandler(options) case "gemini": return new GeminiHandler(options) case "openai-native": diff --git a/src/api/providers/lmstudio.ts b/src/api/providers/lmstudio.ts new file mode 100644 index 0000000..1c085f7 --- /dev/null +++ b/src/api/providers/lmstudio.ts @@ -0,0 +1,56 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" +import { ApiHandler } from "../" +import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { ApiStream } from "../transform/stream" + +export class LmStudioHandler implements ApiHandler { + private options: ApiHandlerOptions + private client: OpenAI + + constructor(options: ApiHandlerOptions) { + this.options = options + this.client = new OpenAI({ + baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1", + apiKey: "noop", + }) + } + + async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...convertToOpenAiMessages(messages), + ] + + try { + const stream = await this.client.chat.completions.create({ + model: this.getModel().id, + messages: openAiMessages, + temperature: 0, + stream: true, + }) + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + if (delta?.content) { + yield { + type: "text", + text: delta.content, + } + } + } + } catch (error) { + // LM Studio doesn't return an error code/body for now + throw new Error( + "Please check the LM Studio developer logs to debug what went wrong. You may need to load the model with a larger context length to work with Cline's prompts." + ) + } + } + + getModel(): { id: string; info: ModelInfo } { + return { + id: this.options.lmStudioModelId || "", + info: openAiModelInfoSaneDefaults, + } + } +} diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 2227c3e..93bbaba 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -31,9 +31,19 @@ export class OpenRouterHandler implements ApiHandler { ] // prompt caching: https://openrouter.ai/docs/prompt-caching + // this is specifically for claude models (some models may 'support prompt caching' automatically without this) switch (this.getModel().id) { + case "anthropic/claude-3.5-sonnet": case "anthropic/claude-3.5-sonnet:beta": + case "anthropic/claude-3.5-sonnet-20240620": + case "anthropic/claude-3.5-sonnet-20240620:beta": + case "anthropic/claude-3-5-haiku": + case "anthropic/claude-3-5-haiku:beta": + case "anthropic/claude-3-5-haiku-20241022": + case "anthropic/claude-3-5-haiku-20241022:beta": + case "anthropic/claude-3-haiku": case "anthropic/claude-3-haiku:beta": + case "anthropic/claude-3-opus": case "anthropic/claude-3-opus:beta": openAiMessages[0] = { role: "system", @@ -76,6 +86,12 @@ export class OpenRouterHandler implements ApiHandler { switch (this.getModel().id) { case "anthropic/claude-3.5-sonnet": case "anthropic/claude-3.5-sonnet:beta": + case "anthropic/claude-3.5-sonnet-20240620": + case "anthropic/claude-3.5-sonnet-20240620:beta": + case "anthropic/claude-3-5-haiku": + case "anthropic/claude-3-5-haiku:beta": + case "anthropic/claude-3-5-haiku-20241022": + case "anthropic/claude-3-5-haiku-20241022:beta": maxTokens = 8_192 break } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index af64214..3df652f 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -53,6 +53,8 @@ type GlobalStateKey = | "openAiModelId" | "ollamaModelId" | "ollamaBaseUrl" + | "lmStudioModelId" + | "lmStudioBaseUrl" | "anthropicBaseUrl" | "azureApiVersion" | "openRouterModelId" @@ -363,6 +365,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiModelId, ollamaModelId, ollamaBaseUrl, + lmStudioModelId, + lmStudioBaseUrl, anthropicBaseUrl, geminiApiKey, openAiNativeApiKey, @@ -386,6 +390,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("openAiModelId", openAiModelId) await this.updateGlobalState("ollamaModelId", ollamaModelId) await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl) + await this.updateGlobalState("lmStudioModelId", lmStudioModelId) + await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl) await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl) await this.storeSecret("geminiApiKey", geminiApiKey) await this.storeSecret("openAiNativeApiKey", openAiNativeApiKey) @@ -460,6 +466,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { 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 "refreshOpenRouterModels": await this.refreshOpenRouterModels() break @@ -527,6 +537,25 @@ export class ClineProvider implements vscode.WebviewViewProvider { } } + // LM Studio + + 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(modelsArray)] + return models + } catch (error) { + return [] + } + } + // OpenRouter async handleOpenRouterCallback(code: string) { @@ -855,6 +884,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiModelId, ollamaModelId, ollamaBaseUrl, + lmStudioModelId, + lmStudioBaseUrl, anthropicBaseUrl, geminiApiKey, openAiNativeApiKey, @@ -884,6 +915,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("openAiModelId") as Promise, this.getGlobalState("ollamaModelId") as Promise, this.getGlobalState("ollamaBaseUrl") as Promise, + this.getGlobalState("lmStudioModelId") as Promise, + this.getGlobalState("lmStudioBaseUrl") as Promise, this.getGlobalState("anthropicBaseUrl") as Promise, this.getSecret("geminiApiKey") as Promise, this.getSecret("openAiNativeApiKey") as Promise, @@ -930,6 +963,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { openAiModelId, ollamaModelId, ollamaBaseUrl, + lmStudioModelId, + lmStudioBaseUrl, anthropicBaseUrl, geminiApiKey, openAiNativeApiKey, diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index d558cfb..6cd9414 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -10,6 +10,7 @@ export interface ExtensionMessage { | "state" | "selectedImages" | "ollamaModels" + | "lmStudioModels" | "theme" | "workspaceUpdated" | "invoke" @@ -21,6 +22,7 @@ export interface ExtensionMessage { state?: ExtensionState images?: string[] ollamaModels?: string[] + lmStudioModels?: string[] filePaths?: string[] partialMessage?: ClineMessage openRouterModels?: Record diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 0ddc861..b89ffec 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -19,6 +19,7 @@ export interface WebviewMessage { | "exportTaskWithId" | "resetState" | "requestOllamaModels" + | "requestLmStudioModels" | "openImage" | "openFile" | "openMention" diff --git a/src/shared/api.ts b/src/shared/api.ts index b2c5525..11bfd9f 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -5,6 +5,7 @@ export type ApiProvider = | "vertex" | "openai" | "ollama" + | "lmstudio" | "gemini" | "openai-native" @@ -27,6 +28,8 @@ export interface ApiHandlerOptions { openAiModelId?: string ollamaModelId?: string ollamaBaseUrl?: string + lmStudioModelId?: string + lmStudioBaseUrl?: string geminiApiKey?: string openAiNativeApiKey?: string azureApiVersion?: string diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 1c8e309..0b3f494 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -96,6 +96,7 @@ const TaskHeader: React.FC = ({ return ( apiConfiguration?.apiProvider !== "openai" && apiConfiguration?.apiProvider !== "ollama" && + apiConfiguration?.apiProvider !== "lmstudio" && apiConfiguration?.apiProvider !== "gemini" ) }, [apiConfiguration?.apiProvider]) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index f5e44ca..dbbda7d 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -45,6 +45,7 @@ interface ApiOptionsProps { const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => { const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState() const [ollamaModels, setOllamaModels] = useState([]) + const [lmStudioModels, setLmStudioModels] = useState([]) const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) @@ -57,23 +58,27 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: return normalizeApiConfiguration(apiConfiguration) }, [apiConfiguration]) - // Poll ollama models - const requestOllamaModels = useCallback(() => { + // Poll ollama/lmstudio models + const requestLocalModels = useCallback(() => { if (selectedProvider === "ollama") { vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl }) + } else if (selectedProvider === "lmstudio") { + vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl }) } - }, [selectedProvider, apiConfiguration?.ollamaBaseUrl]) + }, [selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl]) useEffect(() => { - if (selectedProvider === "ollama") { - requestOllamaModels() + if (selectedProvider === "ollama" || selectedProvider === "lmstudio") { + requestLocalModels() } - }, [selectedProvider, requestOllamaModels]) - useInterval(requestOllamaModels, selectedProvider === "ollama" ? 2000 : null) + }, [selectedProvider, requestLocalModels]) + useInterval(requestLocalModels, selectedProvider === "ollama" || selectedProvider === "lmstudio" ? 2000 : null) const handleMessage = useCallback((event: MessageEvent) => { const message: ExtensionMessage = event.data if (message.type === "ollamaModels" && message.ollamaModels) { setOllamaModels(message.ollamaModels) + } else if (message.type === "lmStudioModels" && message.lmStudioModels) { + setLmStudioModels(message.lmStudioModels) } }, []) useEvent("message", handleMessage) @@ -128,6 +133,7 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: AWS Bedrock OpenAI OpenAI Compatible + LM Studio Ollama @@ -463,6 +469,75 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: )} + {selectedProvider === "lmstudio" && ( +
+ + Base URL (optional) + + + Model ID + + {lmStudioModels.length > 0 && ( + { + const value = (e.target as HTMLInputElement)?.value + // need to check value first since radio group returns empty string sometimes + if (value) { + handleInputChange("lmStudioModelId")({ + target: { value }, + }) + } + }}> + {lmStudioModels.map((model) => ( + + {model} + + ))} + + )} +

+ LM Studio allows you to run models locally on your computer. For instructions on how to get + started, see their + + quickstart guide. + + You will also need to start LM Studio's{" "} + + local server + {" "} + feature to use it with this extension.{" "} + + (Note: Cline uses complex prompts and works best + with Claude models. Less capable models may not work as expected.) + +

+
+ )} + {selectedProvider === "ollama" && (
@@ -758,6 +834,12 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) { selectedModelId: apiConfiguration?.ollamaModelId || "", selectedModelInfo: openAiModelInfoSaneDefaults, } + case "lmstudio": + return { + selectedProvider: provider, + selectedModelId: apiConfiguration?.lmStudioModelId || "", + selectedModelInfo: openAiModelInfoSaneDefaults, + } default: return getProviderData(anthropicModels, anthropicDefaultModelId) } diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 2bb6833..9314cf1 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -56,6 +56,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode config.vertexProjectId, config.openAiApiKey, config.ollamaModelId, + config.lmStudioModelId, config.geminiApiKey, config.openAiNativeApiKey, ].some((key) => key !== undefined) diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 060e0f6..df21fb1 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -47,6 +47,11 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s return "You must provide a valid model ID." } break + case "lmstudio": + if (!apiConfiguration.lmStudioModelId) { + return "You must provide a valid model ID." + } + break } } return undefined