From 6c6087e9a2d7c200cdbd19599b328b88ba9af45e Mon Sep 17 00:00:00 2001
From: cte
Date: Mon, 24 Feb 2025 23:53:01 -0800
Subject: [PATCH 01/32] v3.7.3
---
.changeset/real-zebras-remain.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/real-zebras-remain.md
diff --git a/.changeset/real-zebras-remain.md b/.changeset/real-zebras-remain.md
new file mode 100644
index 0000000000..94a1dc5d36
--- /dev/null
+++ b/.changeset/real-zebras-remain.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+v3.7.3
From 2655bebc8fb9abc1ebd923249286bf92b40e72ad Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Tue, 25 Feb 2025 07:57:37 +0000
Subject: [PATCH 02/32] changeset version bump
---
.changeset/real-zebras-remain.md | 5 -----
CHANGELOG.md | 6 ++++++
package-lock.json | 4 ++--
package.json | 2 +-
4 files changed, 9 insertions(+), 8 deletions(-)
delete mode 100644 .changeset/real-zebras-remain.md
diff --git a/.changeset/real-zebras-remain.md b/.changeset/real-zebras-remain.md
deleted file mode 100644
index 94a1dc5d36..0000000000
--- a/.changeset/real-zebras-remain.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-v3.7.3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 381e0907eb..df70430226 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Roo Code Changelog
+## 3.7.3
+
+### Patch Changes
+
+- v3.7.3
+
## [3.7.2]
- Fix computer use and prompt caching for OpenRouter's `anthropic/claude-3.7-sonnet:beta` (thanks @cte!)
diff --git a/package-lock.json b/package-lock.json
index ba152e81f3..71e3aa8648 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.2",
+ "version": "3.7.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.2",
+ "version": "3.7.3",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 651de2b764..5166affcaa 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Code (prev. Roo Cline)",
"description": "An AI-powered autonomous coding agent that lives in your editor.",
"publisher": "RooVeterinaryInc",
- "version": "3.7.2",
+ "version": "3.7.3",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From 8ba179d3b00fbdb528738b996fad6e88eab9fd43 Mon Sep 17 00:00:00 2001
From: R00-B0T
Date: Tue, 25 Feb 2025 07:58:01 +0000
Subject: [PATCH 03/32] Updating CHANGELOG.md format
---
CHANGELOG.md | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index df70430226..f3a0db08a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,6 @@
# Roo Code Changelog
-## 3.7.3
-
-### Patch Changes
+## [3.7.3]
- v3.7.3
From be7f71c49890d7176c19e7b14da16493557437e5 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Mon, 24 Feb 2025 23:58:45 -0800
Subject: [PATCH 04/32] Update CHANGELOG.md
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3a0db08a2..fc9b387f7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
## [3.7.3]
-- v3.7.3
+- Support for Sonnet 3.7 "Thinking".
## [3.7.2]
From 613f74b717247ff4112452ff9152207418c4e71a Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Tue, 25 Feb 2025 00:05:04 -0800
Subject: [PATCH 05/32] Update CHANGELOG.md
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc9b387f7f..f72dd8a0ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
## [3.7.3]
-- Support for Sonnet 3.7 "Thinking".
+- Support for ["Thinking"](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking) Sonnet 3.7 when using the Anthropic provider.
## [3.7.2]
From c6092ce170492ae99bbb3fffe39706212474f17f Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 01:03:12 -0800
Subject: [PATCH 06/32] Properly reset thinking setting when changing profiles
---
.../src/components/settings/ApiOptions.tsx | 24 +++++++++----------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 0b6a118656..8c2f382db6 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -68,15 +68,17 @@ const ApiOptions = ({
const [lmStudioModels, setLmStudioModels] = useState([])
const [vsCodeLmModels, setVsCodeLmModels] = useState([])
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
- const [anthropicThinkingBudget, setAnthropicThinkingBudget] = useState(apiConfiguration?.anthropicThinking)
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
- const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any
+ const anthropicThinkingBudget = apiConfiguration?.anthropicThinking
+
const noTransform = (value: T) => value
+ const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any
const dropdownEventTransform = (event: DropdownOption | string | undefined) =>
(typeof event == "string" ? event : event?.value) as T
+
const handleInputChange = useCallback(
(
field: K,
@@ -107,8 +109,10 @@ const ApiOptions = ({
250,
[selectedProvider, apiConfiguration?.ollamaBaseUrl, apiConfiguration?.lmStudioBaseUrl],
)
+
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
+
if (message.type === "ollamaModels" && Array.isArray(message.ollamaModels)) {
const newModels = message.ollamaModels
setOllamaModels(newModels)
@@ -120,6 +124,7 @@ const ApiOptions = ({
setVsCodeLmModels(newModels)
}
}, [])
+
useEvent("message", handleMessage)
const createDropdown = (models: Record) => {
@@ -130,6 +135,7 @@ const ApiOptions = ({
label: modelId,
})),
]
+
return (
{
- const budget = checked ? 16_384 : undefined
- setAnthropicThinkingBudget(budget)
- setApiConfigurationField("anthropicThinking", budget)
- }}>
+ onChange={(checked) =>
+ setApiConfigurationField("anthropicThinking", checked ? 16_384 : undefined)
+ }>
Thinking?
{anthropicThinkingBudget && (
@@ -1286,11 +1290,7 @@ const ApiOptions = ({
max={anthropicModels["claude-3-7-sonnet-20250219"].maxTokens - 1}
step={1024}
value={[anthropicThinkingBudget]}
- onValueChange={(value) => {
- const budget = value[0]
- setAnthropicThinkingBudget(budget)
- setApiConfigurationField("anthropicThinking", budget)
- }}
+ onValueChange={(value) => setApiConfigurationField("anthropicThinking", value[0])}
/>
{anthropicThinkingBudget}
From 3621deb725937a5b97fcc62a49c7e464b67501f1 Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 01:04:31 -0800
Subject: [PATCH 07/32] Add changeset
---
.changeset/little-deers-occur.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/little-deers-occur.md
diff --git a/.changeset/little-deers-occur.md b/.changeset/little-deers-occur.md
new file mode 100644
index 0000000000..e699d277c1
--- /dev/null
+++ b/.changeset/little-deers-occur.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles.
From 53ead4c886b236db2d744e1c5148112a246aff18 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Tue, 25 Feb 2025 14:19:17 +0000
Subject: [PATCH 08/32] changeset version bump
---
.changeset/little-deers-occur.md | 5 -----
CHANGELOG.md | 6 ++++++
package-lock.json | 4 ++--
package.json | 2 +-
4 files changed, 9 insertions(+), 8 deletions(-)
delete mode 100644 .changeset/little-deers-occur.md
diff --git a/.changeset/little-deers-occur.md b/.changeset/little-deers-occur.md
deleted file mode 100644
index e699d277c1..0000000000
--- a/.changeset/little-deers-occur.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f72dd8a0ca..2586e8540f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Roo Code Changelog
+## 3.7.4
+
+### Patch Changes
+
+- Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles.
+
## [3.7.3]
- Support for ["Thinking"](https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking) Sonnet 3.7 when using the Anthropic provider.
diff --git a/package-lock.json b/package-lock.json
index 71e3aa8648..4bcdf8136d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.3",
+ "version": "3.7.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.3",
+ "version": "3.7.4",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 5166affcaa..84bec2645a 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Code (prev. Roo Cline)",
"description": "An AI-powered autonomous coding agent that lives in your editor.",
"publisher": "RooVeterinaryInc",
- "version": "3.7.3",
+ "version": "3.7.4",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From df4d2a549c6ac1464de40cfc8bd8340da5ead9d1 Mon Sep 17 00:00:00 2001
From: R00-B0T
Date: Tue, 25 Feb 2025 14:19:45 +0000
Subject: [PATCH 09/32] Updating CHANGELOG.md format
---
CHANGELOG.md | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2586e8540f..52fb754097 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,6 @@
# Roo Code Changelog
-## 3.7.4
-
-### Patch Changes
+## [3.7.4]
- Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles.
From d5b796263dd160d4f6fe0ccc0ad8c464c97ec01f Mon Sep 17 00:00:00 2001
From: System233
Date: Wed, 26 Feb 2025 02:44:57 +0800
Subject: [PATCH 10/32] Add a combobox component with auto-complete
functionality
---
webview-ui/src/__mocks__/lucide-react.ts | 6 +
.../src/components/ui/combobox-primitive.tsx | 522 ++++++++++++++++++
webview-ui/src/components/ui/combobox.tsx | 177 ++++++
webview-ui/src/components/ui/input-base.tsx | 157 ++++++
4 files changed, 862 insertions(+)
create mode 100644 webview-ui/src/__mocks__/lucide-react.ts
create mode 100644 webview-ui/src/components/ui/combobox-primitive.tsx
create mode 100644 webview-ui/src/components/ui/combobox.tsx
create mode 100644 webview-ui/src/components/ui/input-base.tsx
diff --git a/webview-ui/src/__mocks__/lucide-react.ts b/webview-ui/src/__mocks__/lucide-react.ts
new file mode 100644
index 0000000000..d85cd25d6a
--- /dev/null
+++ b/webview-ui/src/__mocks__/lucide-react.ts
@@ -0,0 +1,6 @@
+import React from "react"
+
+export const Check = () => React.createElement("div")
+export const ChevronsUpDown = () => React.createElement("div")
+export const Loader = () => React.createElement("div")
+export const X = () => React.createElement("div")
diff --git a/webview-ui/src/components/ui/combobox-primitive.tsx b/webview-ui/src/components/ui/combobox-primitive.tsx
new file mode 100644
index 0000000000..13bad87aba
--- /dev/null
+++ b/webview-ui/src/components/ui/combobox-primitive.tsx
@@ -0,0 +1,522 @@
+/* eslint-disable react/jsx-pascal-case */
+"use client"
+
+import * as React from "react"
+import { composeEventHandlers } from "@radix-ui/primitive"
+import { useComposedRefs } from "@radix-ui/react-compose-refs"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+import { Primitive } from "@radix-ui/react-primitive"
+import * as RovingFocusGroupPrimitive from "@radix-ui/react-roving-focus"
+import { useControllableState } from "@radix-ui/react-use-controllable-state"
+import { Command as CommandPrimitive } from "cmdk"
+
+export type ComboboxContextProps = {
+ inputValue: string
+ onInputValueChange: (inputValue: string, reason: "inputChange" | "itemSelect" | "clearClick") => void
+ onInputBlur?: (e: React.FocusEvent) => void
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ currentTabStopId: string | null
+ onCurrentTabStopIdChange: (currentTabStopId: string | null) => void
+ inputRef: React.RefObject
+ tagGroupRef: React.RefObject>
+ disabled?: boolean
+ required?: boolean
+} & (
+ | Required>
+ | Required>
+)
+
+const ComboboxContext = React.createContext({
+ type: "single",
+ value: "",
+ onValueChange: () => {},
+ inputValue: "",
+ onInputValueChange: () => {},
+ onInputBlur: () => {},
+ open: false,
+ onOpenChange: () => {},
+ currentTabStopId: null,
+ onCurrentTabStopIdChange: () => {},
+ inputRef: { current: null },
+ tagGroupRef: { current: null },
+ disabled: false,
+ required: false,
+})
+
+export const useComboboxContext = () => React.useContext(ComboboxContext)
+
+export type ComboboxType = "single" | "multiple"
+
+export interface ComboboxBaseProps
+ extends React.ComponentProps,
+ Omit, "value" | "defaultValue" | "onValueChange"> {
+ type?: ComboboxType | undefined
+ inputValue?: string
+ defaultInputValue?: string
+ onInputValueChange?: (inputValue: string, reason: "inputChange" | "itemSelect" | "clearClick") => void
+ onInputBlur?: (e: React.FocusEvent) => void
+ disabled?: boolean
+ required?: boolean
+}
+
+export type ComboboxValue = T extends "single"
+ ? string
+ : T extends "multiple"
+ ? string[]
+ : never
+
+export interface ComboboxSingleProps {
+ type: "single"
+ value?: string
+ defaultValue?: string
+ onValueChange?: (value: string) => void
+}
+
+export interface ComboboxMultipleProps {
+ type: "multiple"
+ value?: string[]
+ defaultValue?: string[]
+ onValueChange?: (value: string[]) => void
+}
+
+export type ComboboxProps = ComboboxBaseProps & (ComboboxSingleProps | ComboboxMultipleProps)
+
+export const Combobox = React.forwardRef(
+ (
+ {
+ type = "single" as T,
+ open: openProp,
+ onOpenChange,
+ defaultOpen,
+ modal,
+ children,
+ value: valueProp,
+ defaultValue,
+ onValueChange,
+ inputValue: inputValueProp,
+ defaultInputValue,
+ onInputValueChange,
+ onInputBlur,
+ disabled,
+ required,
+ ...props
+ }: ComboboxProps,
+ ref: React.ForwardedRef>,
+ ) => {
+ const [value = type === "multiple" ? [] : "", setValue] = useControllableState>({
+ prop: valueProp as ComboboxValue,
+ defaultProp: defaultValue as ComboboxValue,
+ onChange: onValueChange as (value: ComboboxValue) => void,
+ })
+ const [inputValue = "", setInputValue] = useControllableState({
+ prop: inputValueProp,
+ defaultProp: defaultInputValue,
+ })
+ const [open = false, setOpen] = useControllableState({
+ prop: openProp,
+ defaultProp: defaultOpen,
+ onChange: onOpenChange,
+ })
+ const [currentTabStopId, setCurrentTabStopId] = React.useState(null)
+ const inputRef = React.useRef(null)
+ const tagGroupRef = React.useRef>(null)
+
+ const handleInputValueChange: ComboboxContextProps["onInputValueChange"] = React.useCallback(
+ (inputValue, reason) => {
+ setInputValue(inputValue)
+ onInputValueChange?.(inputValue, reason)
+ },
+ [setInputValue, onInputValueChange],
+ )
+
+ return (
+
+
+
+ {children}
+ {!open && }
+
+
+
+ )
+ },
+)
+Combobox.displayName = "Combobox"
+
+export const ComboboxTagGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => {
+ const { currentTabStopId, onCurrentTabStopIdChange, tagGroupRef, type } = useComboboxContext()
+
+ if (type !== "multiple") {
+ throw new Error(' should only be used when type is "multiple"')
+ }
+
+ const composedRefs = useComposedRefs(ref, tagGroupRef)
+
+ return (
+ onCurrentTabStopIdChange(null)}
+ {...props}
+ />
+ )
+})
+ComboboxTagGroup.displayName = "ComboboxTagGroup"
+
+export interface ComboboxTagGroupItemProps
+ extends React.ComponentPropsWithoutRef {
+ value: string
+ disabled?: boolean
+}
+
+const ComboboxTagGroupItemContext = React.createContext>({
+ value: "",
+ disabled: false,
+})
+
+const useComboboxTagGroupItemContext = () => React.useContext(ComboboxTagGroupItemContext)
+
+export const ComboboxTagGroupItem = React.forwardRef<
+ React.ElementRef,
+ ComboboxTagGroupItemProps
+>(({ onClick, onKeyDown, value: valueProp, disabled, ...props }, ref) => {
+ const { value, onValueChange, inputRef, currentTabStopId, type } = useComboboxContext()
+
+ if (type !== "multiple") {
+ throw new Error(' should only be used when type is "multiple"')
+ }
+
+ const lastItemValue = value.at(-1)
+
+ return (
+
+ {
+ if (event.key === "Escape") {
+ inputRef.current?.focus()
+ }
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") {
+ event.preventDefault()
+ inputRef.current?.focus()
+ }
+ if (event.key === "ArrowRight" && currentTabStopId === lastItemValue) {
+ inputRef.current?.focus()
+ }
+ if (event.key === "Backspace" || event.key === "Delete") {
+ onValueChange(value.filter((v) => v !== currentTabStopId))
+ inputRef.current?.focus()
+ }
+ })}
+ onClick={composeEventHandlers(onClick, () => disabled && inputRef.current?.focus())}
+ tabStopId={valueProp}
+ focusable={!disabled}
+ data-disabled={disabled}
+ active={valueProp === lastItemValue}
+ {...props}
+ />
+
+ )
+})
+ComboboxTagGroupItem.displayName = "ComboboxTagGroupItem"
+
+export const ComboboxTagGroupItemRemove = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ onClick, ...props }, ref) => {
+ const { value, onValueChange, type } = useComboboxContext()
+
+ if (type !== "multiple") {
+ throw new Error(' should only be used when type is "multiple"')
+ }
+
+ const { value: valueProp, disabled } = useComboboxTagGroupItemContext()
+
+ return (
+ onValueChange(value.filter((v) => v !== valueProp)))}
+ {...props}
+ />
+ )
+})
+ComboboxTagGroupItemRemove.displayName = "ComboboxTagGroupItemRemove"
+
+export const ComboboxInput = React.forwardRef<
+ React.ElementRef,
+ Omit, "value" | "onValueChange">
+>(({ onKeyDown, onMouseDown, onFocus, onBlur, ...props }, ref) => {
+ const {
+ type,
+ inputValue,
+ onInputValueChange,
+ onInputBlur,
+ open,
+ onOpenChange,
+ value,
+ onValueChange,
+ inputRef,
+ disabled,
+ required,
+ tagGroupRef,
+ } = useComboboxContext()
+
+ const composedRefs = useComposedRefs(ref, inputRef)
+
+ return (
+ {
+ if (!open) {
+ onOpenChange(true)
+ }
+ // Schedule input value change to the next tick.
+ setTimeout(() => onInputValueChange(search, "inputChange"))
+ if (!search && type === "single") {
+ onValueChange("")
+ }
+ }}
+ onKeyDown={composeEventHandlers(onKeyDown, (event) => {
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") {
+ if (!open) {
+ event.preventDefault()
+ onOpenChange(true)
+ }
+ }
+ if (type !== "multiple") {
+ return
+ }
+ if (event.key === "ArrowLeft" && !inputValue && value.length) {
+ tagGroupRef.current?.focus()
+ }
+ if (event.key === "Backspace" && !inputValue) {
+ onValueChange(value.slice(0, -1))
+ }
+ })}
+ onMouseDown={composeEventHandlers(onMouseDown, () => onOpenChange(!!inputValue || !open))}
+ onFocus={composeEventHandlers(onFocus, () => onOpenChange(true))}
+ onBlur={composeEventHandlers(onBlur, (event) => {
+ if (!event.relatedTarget?.hasAttribute("cmdk-list")) {
+ onInputBlur?.(event)
+ }
+ })}
+ {...props}
+ />
+ )
+})
+ComboboxInput.displayName = "ComboboxInput"
+
+export const ComboboxClear = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ onClick, ...props }, ref) => {
+ const { value, onValueChange, inputValue, onInputValueChange, type } = useComboboxContext()
+
+ const isValueEmpty = type === "single" ? !value : !value.length
+
+ return (
+ {
+ if (type === "single") {
+ onValueChange("")
+ } else {
+ onValueChange([])
+ }
+ onInputValueChange("", "clearClick")
+ })}
+ {...props}
+ />
+ )
+})
+ComboboxClear.displayName = "ComboboxClear"
+
+export const ComboboxTrigger = PopoverPrimitive.Trigger
+
+export const ComboboxAnchor = PopoverPrimitive.Anchor
+
+export const ComboboxPortal = PopoverPrimitive.Portal
+
+export const ComboboxContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ children, onOpenAutoFocus, onInteractOutside, ...props }, ref) => (
+ event.preventDefault())}
+ onCloseAutoFocus={composeEventHandlers(onOpenAutoFocus, (event) => event.preventDefault())}
+ onInteractOutside={composeEventHandlers(onInteractOutside, (event) => {
+ if (event.target instanceof Element && event.target.hasAttribute("cmdk-input")) {
+ event.preventDefault()
+ }
+ })}
+ {...props}>
+ {children}
+
+))
+ComboboxContent.displayName = "ComboboxContent"
+
+export const ComboboxEmpty = CommandPrimitive.Empty
+
+export const ComboboxLoading = CommandPrimitive.Loading
+
+export interface ComboboxItemProps extends Omit, "value"> {
+ value: string
+}
+
+const ComboboxItemContext = React.createContext({ isSelected: false })
+
+const useComboboxItemContext = () => React.useContext(ComboboxItemContext)
+
+const findComboboxItemText = (children: React.ReactNode) => {
+ let text = ""
+
+ React.Children.forEach(children, (child) => {
+ if (text) {
+ return
+ }
+
+ if (React.isValidElement<{ children: React.ReactNode }>(child)) {
+ if (child.type === ComboboxItemText) {
+ text = child.props.children as string
+ } else {
+ text = findComboboxItemText(child.props.children)
+ }
+ }
+ })
+
+ return text
+}
+
+export const ComboboxItem = React.forwardRef, ComboboxItemProps>(
+ ({ value: valueProp, children, onMouseDown, ...props }, ref) => {
+ const { type, value, onValueChange, onInputValueChange, onOpenChange } = useComboboxContext()
+
+ const inputValue = React.useMemo(() => findComboboxItemText(children), [children])
+
+ const isSelected = type === "single" ? value === valueProp : value.includes(valueProp)
+
+ return (
+
+ event.preventDefault())}
+ onSelect={() => {
+ if (type === "multiple") {
+ onValueChange(
+ value.includes(valueProp)
+ ? value.filter((v) => v !== valueProp)
+ : [...value, valueProp],
+ )
+ onInputValueChange("", "itemSelect")
+ } else {
+ onValueChange(valueProp)
+ onInputValueChange(inputValue, "itemSelect")
+ // Schedule open change to the next tick.
+ setTimeout(() => onOpenChange(false))
+ }
+ }}
+ value={inputValue}
+ {...props}>
+ {children}
+
+
+ )
+ },
+)
+ComboboxItem.displayName = "ComboboxItem"
+
+export const ComboboxItemIndicator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => {
+ const { isSelected } = useComboboxItemContext()
+
+ if (!isSelected) {
+ return null
+ }
+
+ return
+})
+ComboboxItemIndicator.displayName = "ComboboxItemIndicator"
+
+export interface ComboboxItemTextProps extends React.ComponentPropsWithoutRef {
+ children: string
+}
+
+export const ComboboxItemText = (props: ComboboxItemTextProps) =>
+ComboboxItemText.displayName = "ComboboxItemText"
+
+export const ComboboxGroup = CommandPrimitive.Group
+
+export const ComboboxSeparator = CommandPrimitive.Separator
+
+const Root = Combobox
+const TagGroup = ComboboxTagGroup
+const TagGroupItem = ComboboxTagGroupItem
+const TagGroupItemRemove = ComboboxTagGroupItemRemove
+const Input = ComboboxInput
+const Clear = ComboboxClear
+const Trigger = ComboboxTrigger
+const Anchor = ComboboxAnchor
+const Portal = ComboboxPortal
+const Content = ComboboxContent
+const Empty = ComboboxEmpty
+const Loading = ComboboxLoading
+const Item = ComboboxItem
+const ItemIndicator = ComboboxItemIndicator
+const ItemText = ComboboxItemText
+const Group = ComboboxGroup
+const Separator = ComboboxSeparator
+
+export {
+ Root,
+ TagGroup,
+ TagGroupItem,
+ TagGroupItemRemove,
+ Input,
+ Clear,
+ Trigger,
+ Anchor,
+ Portal,
+ Content,
+ Empty,
+ Loading,
+ Item,
+ ItemIndicator,
+ ItemText,
+ Group,
+ Separator,
+}
diff --git a/webview-ui/src/components/ui/combobox.tsx b/webview-ui/src/components/ui/combobox.tsx
new file mode 100644
index 0000000000..24b2f7be1f
--- /dev/null
+++ b/webview-ui/src/components/ui/combobox.tsx
@@ -0,0 +1,177 @@
+"use client"
+
+import * as React from "react"
+import { Slottable } from "@radix-ui/react-slot"
+import { cva } from "class-variance-authority"
+import { Check, ChevronsUpDown, Loader, X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import * as ComboboxPrimitive from "@/components/ui/combobox-primitive"
+import { badgeVariants } from "@/components/ui/badge"
+// import * as ComboboxPrimitive from "@/registry/default/ui/combobox-primitive"
+import {
+ InputBase,
+ InputBaseAdornmentButton,
+ InputBaseControl,
+ InputBaseFlexWrapper,
+ InputBaseInput,
+} from "@/components/ui/input-base"
+
+export const Combobox = ComboboxPrimitive.Root
+
+const ComboboxInputBase = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+))
+ComboboxInputBase.displayName = "ComboboxInputBase"
+
+export const ComboboxInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+
+
+
+
+
+
+))
+ComboboxInput.displayName = "ComboboxInput"
+
+export const ComboboxTagsInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+))
+ComboboxTagsInput.displayName = "ComboboxTagsInput"
+
+export const ComboboxTag = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ children, className, ...props }, ref) => (
+
+ {children}
+
+
+ Remove
+
+
+))
+ComboboxTag.displayName = "ComboboxTag"
+
+export const ComboboxContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "start", alignOffset = 0, ...props }, ref) => (
+
+
+
+))
+ComboboxContent.displayName = "ComboboxContent"
+
+export const ComboboxEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ComboboxEmpty.displayName = "ComboboxEmpty"
+
+export const ComboboxLoading = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+ComboboxLoading.displayName = "ComboboxLoading"
+
+export const ComboboxGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ComboboxGroup.displayName = "ComboboxGroup"
+
+const ComboboxSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ComboboxSeparator.displayName = "ComboboxSeparator"
+
+export const comboboxItemStyle = cva(
+ "relative flex w-full cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-vscode-dropdown-foreground data-[disabled=true]:opacity-50",
+)
+
+export const ComboboxItem = React.forwardRef<
+ React.ElementRef,
+ Omit, "children"> &
+ Pick, "children">
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+
+
+
+))
+ComboboxItem.displayName = "ComboboxItem"
diff --git a/webview-ui/src/components/ui/input-base.tsx b/webview-ui/src/components/ui/input-base.tsx
new file mode 100644
index 0000000000..9dbda6eb13
--- /dev/null
+++ b/webview-ui/src/components/ui/input-base.tsx
@@ -0,0 +1,157 @@
+/* eslint-disable react/jsx-no-comment-textnodes */
+/* eslint-disable react/jsx-pascal-case */
+"use client"
+
+import * as React from "react"
+import { composeEventHandlers } from "@radix-ui/primitive"
+import { composeRefs } from "@radix-ui/react-compose-refs"
+import { Primitive } from "@radix-ui/react-primitive"
+import { Slot } from "@radix-ui/react-slot"
+
+import { cn } from "@/lib/utils"
+import { Button } from "./button"
+
+export type InputBaseContextProps = Pick & {
+ controlRef: React.RefObject
+ onFocusedChange: (focused: boolean) => void
+}
+
+const InputBaseContext = React.createContext({
+ autoFocus: false,
+ controlRef: { current: null },
+ disabled: false,
+ onFocusedChange: () => {},
+})
+
+const useInputBaseContext = () => React.useContext(InputBaseContext)
+
+export interface InputBaseProps extends React.ComponentPropsWithoutRef {
+ autoFocus?: boolean
+ disabled?: boolean
+}
+
+export const InputBase = React.forwardRef, InputBaseProps>(
+ ({ autoFocus, disabled, className, onClick, ...props }, ref) => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [focused, setFocused] = React.useState(false)
+
+ const controlRef = React.useRef(null)
+
+ return (
+
+ {
+ // Based on MUI's implementation.
+ // https://github.com/mui/material-ui/blob/master/packages/mui-material/src/InputBase/InputBase.js#L458~L460
+ if (controlRef.current && event.currentTarget === event.target) {
+ controlRef.current.focus()
+ }
+ })}
+ className={cn(
+ "flex w-full text-vscode-input-foreground border border-vscode-dropdown-border bg-vscode-input-background rounded-xs px-3 py-0.5 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus:outline-0 focus-visible:outline-none focus-visible:border-vscode-focusBorder disabled:cursor-not-allowed disabled:opacity-50",
+ disabled && "cursor-not-allowed opacity-50",
+ className,
+ )}
+ {...props}
+ />
+
+ )
+ },
+)
+InputBase.displayName = "InputBase"
+
+export const InputBaseFlexWrapper = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+InputBaseFlexWrapper.displayName = "InputBaseFlexWrapper"
+
+export const InputBaseControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ onFocus, onBlur, ...props }, ref) => {
+ const { controlRef, autoFocus, disabled, onFocusedChange } = useInputBaseContext()
+
+ return (
+ onFocusedChange(true))}
+ onBlur={composeEventHandlers(onBlur, () => onFocusedChange(false))}
+ {...{ disabled }}
+ {...props}
+ />
+ )
+})
+InputBaseControl.displayName = "InputBaseControl"
+
+export interface InputBaseAdornmentProps extends React.ComponentPropsWithoutRef<"div"> {
+ asChild?: boolean
+ disablePointerEvents?: boolean
+}
+
+export const InputBaseAdornment = React.forwardRef, InputBaseAdornmentProps>(
+ ({ className, disablePointerEvents, asChild, children, ...props }, ref) => {
+ const Comp = asChild ? Slot : typeof children === "string" ? "p" : "div"
+
+ const isAction = React.isValidElement(children) && children.type === InputBaseAdornmentButton
+
+ return (
+
+ {children}
+
+ )
+ },
+)
+InputBaseAdornment.displayName = "InputBaseAdornment"
+
+export const InputBaseAdornmentButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ type = "button", variant = "ghost", size = "icon", disabled: disabledProp, className, ...props }, ref) => {
+ const { disabled } = useInputBaseContext()
+
+ return (
+
+ )
+})
+InputBaseAdornmentButton.displayName = "InputBaseAdornmentButton"
+
+export const InputBaseInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+InputBaseInput.displayName = "InputBaseInput"
From 159621cea6cfe11e3245ab5b4becf70c63e8fcf2 Mon Sep 17 00:00:00 2001
From: Chris Estreich
Date: Tue, 25 Feb 2025 11:35:24 -0800
Subject: [PATCH 11/32] Move & clean up model fetchers
---
src/api/providers/glama.ts | 34 +-
src/api/providers/lmstudio.ts | 16 +
src/api/providers/ollama.ts | 16 +
src/api/providers/openai.ts | 25 +
src/api/providers/openrouter.ts | 86 ++-
src/api/providers/requesty.ts | 44 +-
src/api/providers/unbound.ts | 34 +-
src/api/providers/vscode-lm.ts | 13 +
src/core/Cline.ts | 6 +-
.../__tests__/CustomModesManager.test.ts | 17 +-
src/core/webview/ClineProvider.ts | 726 +++++-------------
src/services/mcp/McpHub.ts | 4 +-
src/shared/globalFileNames.ts | 9 +
src/shared/globalState.ts | 85 ++
src/utils/__tests__/path.test.ts | 34 +-
src/utils/cost.ts | 2 +
webview-ui/src/components/chat/TaskHeader.tsx | 2 +-
17 files changed, 568 insertions(+), 585 deletions(-)
create mode 100644 src/shared/globalFileNames.ts
create mode 100644 src/shared/globalState.ts
diff --git a/src/api/providers/glama.ts b/src/api/providers/glama.ts
index 72b41e5f58..e1cf62b152 100644
--- a/src/api/providers/glama.ts
+++ b/src/api/providers/glama.ts
@@ -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,33 @@ export class GlamaHandler implements ApiHandler, SingleCompletionHandler {
}
}
}
+
+export async function getGlamaModels() {
+ const models: Record = {}
+
+ 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),
+ }
+
+ models[rawModel.id] = modelInfo
+ }
+ } catch (error) {
+ console.error(`Error fetching Glama models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
+ }
+
+ return models
+}
diff --git a/src/api/providers/lmstudio.ts b/src/api/providers/lmstudio.ts
index 7efa037f46..beb3bd1b79 100644
--- a/src/api/providers/lmstudio.ts
+++ b/src/api/providers/lmstudio.ts
@@ -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(modelsArray)]
+ } catch (error) {
+ return []
+ }
+}
diff --git a/src/api/providers/ollama.ts b/src/api/providers/ollama.ts
index afb6117b54..de7df5d261 100644
--- a/src/api/providers/ollama.ts
+++ b/src/api/providers/ollama.ts
@@ -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(modelsArray)]
+ } catch (error) {
+ return []
+ }
+}
diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts
index cea500df26..f1c404d50a 100644
--- a/src/api/providers/openai.ts
+++ b/src/api/providers/openai.ts
@@ -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 = {}
+
+ 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(modelsArray)]
+ } catch (error) {
+ return []
+ }
+}
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index eb9e819d77..2947c499fa 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -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
@@ -251,3 +251,75 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
}
}
}
+
+export async function getOpenRouterModels() {
+ const models: Record = {}
+
+ 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,
+ }
+
+ switch (rawModel.id) {
+ case "anthropic/claude-3.7-sonnet":
+ case "anthropic/claude-3.7-sonnet:beta":
+ case "anthropic/claude-3.5-sonnet":
+ case "anthropic/claude-3.5-sonnet:beta":
+ // NOTE: This needs to be synced with api.ts/openrouter default model info.
+ modelInfo.supportsComputerUse = true
+ modelInfo.supportsPromptCache = true
+ modelInfo.cacheWritesPrice = 3.75
+ modelInfo.cacheReadsPrice = 0.3
+ break
+ case "anthropic/claude-3.5-sonnet-20240620":
+ case "anthropic/claude-3.5-sonnet-20240620:beta":
+ modelInfo.supportsPromptCache = true
+ modelInfo.cacheWritesPrice = 3.75
+ modelInfo.cacheReadsPrice = 0.3
+ break
+ 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.5-haiku":
+ case "anthropic/claude-3.5-haiku:beta":
+ case "anthropic/claude-3.5-haiku-20241022":
+ case "anthropic/claude-3.5-haiku-20241022:beta":
+ modelInfo.supportsPromptCache = true
+ modelInfo.cacheWritesPrice = 1.25
+ modelInfo.cacheReadsPrice = 0.1
+ break
+ case "anthropic/claude-3-opus":
+ case "anthropic/claude-3-opus:beta":
+ modelInfo.supportsPromptCache = true
+ modelInfo.cacheWritesPrice = 18.75
+ modelInfo.cacheReadsPrice = 1.5
+ break
+ case "anthropic/claude-3-haiku":
+ case "anthropic/claude-3-haiku:beta":
+ modelInfo.supportsPromptCache = true
+ modelInfo.cacheWritesPrice = 0.3
+ modelInfo.cacheReadsPrice = 0.03
+ break
+ }
+
+ models[rawModel.id] = modelInfo
+ }
+ } catch (error) {
+ console.error(
+ `Error fetching OpenRouter models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
+ )
+ }
+
+ return models
+}
diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts
index 67f43aabc5..5c3c9b16ba 100644
--- a/src/api/providers/requesty.ts
+++ b/src/api/providers/requesty.ts
@@ -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,40 @@ export class RequestyHandler extends OpenAiHandler {
}
}
}
+
+export async function getRequestyModels({ apiKey }: { apiKey?: string }) {
+ const models: Record = {}
+
+ if (!apiKey) {
+ return models
+ }
+
+ try {
+ const config: Record = {}
+ 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),
+ }
+
+ models[rawModel.id] = modelInfo
+ }
+ } catch (error) {
+ console.error(`Error fetching Requesty models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
+ }
+
+ return models
+}
diff --git a/src/api/providers/unbound.ts b/src/api/providers/unbound.ts
index 0599ffa443..0532e00176 100644
--- a/src/api/providers/unbound.ts
+++ b/src/api/providers/unbound.ts
@@ -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,33 @@ export class UnboundHandler implements ApiHandler, SingleCompletionHandler {
}
}
}
+
+export async function getUnboundModels() {
+ const models: Record = {}
+
+ try {
+ const response = await axios.get("https://api.getunbound.ai/models")
+
+ if (response.data) {
+ const rawModels: Record = response.data
+
+ for (const [modelId, model] of Object.entries(rawModels)) {
+ models[modelId] = {
+ 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,
+ }
+ }
+ }
+ } catch (error) {
+ console.error(`Error fetching Unbound models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`)
+ }
+
+ return models
+}
diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts
index e2bf8609ae..28e24231a2 100644
--- a/src/api/providers/vscode-lm.ts
+++ b/src/api/providers/vscode-lm.ts
@@ -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 []
+ }
+}
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 549cec5eea..073bd10911 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -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"
diff --git a/src/core/config/__tests__/CustomModesManager.test.ts b/src/core/config/__tests__/CustomModesManager.test.ts
index 3c8236e920..4031bff906 100644
--- a/src/core/config/__tests__/CustomModesManager.test.ts
+++ b/src/core/config/__tests__/CustomModesManager.test.ts
@@ -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({
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index bb31be5dce..9f973594e6 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -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,159 +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(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 | 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(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 = {}
- 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(modelsArray)]
- return models
- } catch (error) {
- return []
- }
- }
-
- // Requesty
- async readRequestyModels(): Promise | 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 = {}
- try {
- const config: Record = {}
- 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),
- }
-
- 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) {
@@ -1970,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 {
- 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
@@ -2005,225 +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 | 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 | undefined> {
- return this.readModelsFromCache(GlobalFileNames.glamaModels)
- }
-
- async refreshGlamaModels() {
- const glamaModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.glamaModels)
-
- const models: Record = {}
- 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),
- }
-
- 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 | undefined> {
- return this.readModelsFromCache(GlobalFileNames.openRouterModels)
- }
-
- async refreshOpenRouterModels() {
- const openRouterModelsFilePath = path.join(
- await this.ensureCacheDirectoryExists(),
- GlobalFileNames.openRouterModels,
- )
-
- const models: Record = {}
-
- 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,
- }
-
- switch (rawModel.id) {
- case "anthropic/claude-3.7-sonnet":
- case "anthropic/claude-3.7-sonnet:beta":
- case "anthropic/claude-3.5-sonnet":
- case "anthropic/claude-3.5-sonnet:beta":
- // NOTE: this needs to be synced with api.ts/openrouter default model info.
- modelInfo.supportsComputerUse = true
- modelInfo.supportsPromptCache = true
- modelInfo.cacheWritesPrice = 3.75
- modelInfo.cacheReadsPrice = 0.3
- break
- case "anthropic/claude-3.5-sonnet-20240620":
- case "anthropic/claude-3.5-sonnet-20240620:beta":
- modelInfo.supportsPromptCache = true
- modelInfo.cacheWritesPrice = 3.75
- modelInfo.cacheReadsPrice = 0.3
- break
- 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.5-haiku":
- case "anthropic/claude-3.5-haiku:beta":
- case "anthropic/claude-3.5-haiku-20241022":
- case "anthropic/claude-3.5-haiku-20241022:beta":
- modelInfo.supportsPromptCache = true
- modelInfo.cacheWritesPrice = 1.25
- modelInfo.cacheReadsPrice = 0.1
- break
- case "anthropic/claude-3-opus":
- case "anthropic/claude-3-opus:beta":
- modelInfo.supportsPromptCache = true
- modelInfo.cacheWritesPrice = 18.75
- modelInfo.cacheReadsPrice = 1.5
- break
- case "anthropic/claude-3-haiku":
- case "anthropic/claude-3-haiku:beta":
- modelInfo.supportsPromptCache = true
- modelInfo.cacheWritesPrice = 0.3
- modelInfo.cacheReadsPrice = 0.03
- 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 | undefined> {
- return this.readModelsFromCache(GlobalFileNames.unboundModels)
- }
-
- async refreshUnboundModels() {
- const unboundModelsFilePath = path.join(await this.ensureCacheDirectoryExists(), GlobalFileNames.unboundModels)
-
- const models: Record = {}
- try {
- const response = await axios.get("https://api.getunbound.ai/models")
-
- if (response.data) {
- const rawModels: Record = response.data
- for (const [modelId, model] of Object.entries(rawModels)) {
- models[modelId] = {
- 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,
- }
- }
- }
- 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<{
@@ -2810,26 +2452,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) {
diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts
index 6e3f39fa7d..6c906c7cf8 100644
--- a/src/services/mcp/McpHub.ts
+++ b/src/services/mcp/McpHub.ts
@@ -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,
diff --git a/src/shared/globalFileNames.ts b/src/shared/globalFileNames.ts
new file mode 100644
index 0000000000..6088e95d99
--- /dev/null
+++ b/src/shared/globalFileNames.ts
@@ -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",
+}
diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts
new file mode 100644
index 0000000000..7b6b4f8274
--- /dev/null
+++ b/src/shared/globalState.ts
@@ -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"
diff --git a/src/utils/__tests__/path.test.ts b/src/utils/__tests__/path.test.ts
index 1d20e86c69..8c8a8cc672 100644
--- a/src/utils/__tests__/path.test.ts
+++ b/src/utils/__tests__/path.test.ts
@@ -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")
})
})
diff --git a/src/utils/cost.ts b/src/utils/cost.ts
index f8f5f2b125..adc2ded0a8 100644
--- a/src/utils/cost.ts
+++ b/src/utils/cost.ts
@@ -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)
diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx
index b35be0cd2a..341855f796 100644
--- a/webview-ui/src/components/chat/TaskHeader.tsx
+++ b/webview-ui/src/components/chat/TaskHeader.tsx
@@ -351,7 +351,7 @@ const TaskActions = ({ item }: { item: HistoryItem | undefined }) => (
- {item?.size && (
+ {!!item?.size && item.size > 0 && (
)}
- {selectedProvider === "glama" && }
- {selectedProvider === "openrouter" && }
- {selectedProvider === "requesty" && }
+ {selectedProvider === "glama" && (
+
+ )}
+
+ {selectedProvider === "openrouter" && (
+
+ )}
+ {selectedProvider === "requesty" && (
+
+ )}
{selectedProvider !== "glama" &&
selectedProvider !== "openrouter" &&
@@ -1260,7 +1401,6 @@ const ApiOptions = ({
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
{selectedProvider === "mistral" && createDropdown(mistralModels)}
-
(
-
-)
diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx
index b21b37ef0f..8fd6d82daa 100644
--- a/webview-ui/src/components/settings/ModelPicker.tsx
+++ b/webview-ui/src/components/settings/ModelPicker.tsx
@@ -1,185 +1,90 @@
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
-import debounce from "debounce"
-import { useMemo, useState, useCallback, useEffect, useRef } from "react"
-import { useMount } from "react-use"
-import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
+import { useMemo, useState, useCallback, useEffect } from "react"
-import { cn } from "@/lib/utils"
-import {
- Button,
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui"
-
-import { useExtensionState } from "../../context/ExtensionStateContext"
-import { vscode } from "../../utils/vscode"
import { normalizeApiConfiguration } from "./ApiOptions"
import { ModelInfoView } from "./ModelInfoView"
+import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
+import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "../ui/combobox"
-type ModelProvider = "glama" | "openRouter" | "unbound" | "requesty" | "openAi"
+type ExtractType = NonNullable<
+ { [K in keyof ApiConfiguration]: Required[K] extends T ? K : never }[keyof ApiConfiguration]
+>
-type ModelKeys = `${T}Models`
-type ConfigKeys = `${T}ModelId`
-type InfoKeys = `${T}ModelInfo`
-type RefreshMessageType = `refresh${Capitalize}Models`
-
-interface ModelPickerProps {
- defaultModelId: string
- modelsKey: ModelKeys
- configKey: ConfigKeys
- infoKey: InfoKeys
- refreshMessageType: RefreshMessageType
- refreshValues?: Record
+type ModelIdKeys = NonNullable<
+ { [K in keyof ApiConfiguration]: K extends `${string}ModelId` ? K : never }[keyof ApiConfiguration]
+>
+declare module "react" {
+ interface CSSProperties {
+ // Allow CSS variables
+ [key: `--${string}`]: string | number
+ }
+}
+interface ModelPickerProps {
+ defaultModelId?: string
+ models: Record | null
+ modelIdKey: ModelIdKeys
+ modelInfoKey: ExtractType
serviceName: string
serviceUrl: string
recommendedModel: string
- allowCustomModel?: boolean
+ apiConfiguration: ApiConfiguration
+ setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void
+ defaultModelInfo?: ModelInfo
}
export const ModelPicker = ({
defaultModelId,
- modelsKey,
- configKey,
- infoKey,
- refreshMessageType,
- refreshValues,
+ models,
+ modelIdKey,
+ modelInfoKey,
serviceName,
serviceUrl,
recommendedModel,
- allowCustomModel = false,
+ apiConfiguration,
+ setApiConfigurationField,
+ defaultModelInfo,
}: ModelPickerProps) => {
- const [customModelId, setCustomModelId] = useState("")
- const [isCustomModel, setIsCustomModel] = useState(false)
- const [open, setOpen] = useState(false)
- const [value, setValue] = useState(defaultModelId)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
- const prevRefreshValuesRef = useRef | undefined>()
- const { apiConfiguration, [modelsKey]: models, onUpdateApiConfig, setApiConfiguration } = useExtensionState()
-
- const modelIds = useMemo(
- () => (Array.isArray(models) ? models : Object.keys(models)).sort((a, b) => a.localeCompare(b)),
- [models],
- )
+ const modelIds = useMemo(() => Object.keys(models ?? {}).sort((a, b) => a.localeCompare(b)), [models])
const { selectedModelId, selectedModelInfo } = useMemo(
() => normalizeApiConfiguration(apiConfiguration),
[apiConfiguration],
)
-
- const onSelectCustomModel = useCallback(
- (modelId: string) => {
- setCustomModelId(modelId)
- const modelInfo = { id: modelId }
- const apiConfig = { ...apiConfiguration, [configKey]: modelId, [infoKey]: modelInfo }
- setApiConfiguration(apiConfig)
- onUpdateApiConfig(apiConfig)
- setValue(modelId)
- setOpen(false)
- setIsCustomModel(false)
- },
- [apiConfiguration, configKey, infoKey, onUpdateApiConfig, setApiConfiguration],
- )
-
const onSelect = useCallback(
(modelId: string) => {
- const modelInfo = Array.isArray(models)
- ? { id: modelId } // For OpenAI models which are just strings
- : models[modelId] // For other models that have full info objects
- const apiConfig = { ...apiConfiguration, [configKey]: modelId, [infoKey]: modelInfo }
- setApiConfiguration(apiConfig)
- onUpdateApiConfig(apiConfig)
- setValue(modelId)
- setOpen(false)
+ const modelInfo = models?.[modelId]
+ setApiConfigurationField(modelIdKey, modelId)
+ setApiConfigurationField(modelInfoKey, modelInfo ?? defaultModelInfo)
},
- [apiConfiguration, configKey, infoKey, models, onUpdateApiConfig, setApiConfiguration],
+ [modelIdKey, modelInfoKey, models, setApiConfigurationField, defaultModelInfo],
)
-
- const debouncedRefreshModels = useMemo(() => {
- return debounce(() => {
- const message = refreshValues
- ? { type: refreshMessageType, values: refreshValues }
- : { type: refreshMessageType }
- vscode.postMessage(message)
- }, 100)
- }, [refreshMessageType, refreshValues])
-
- useMount(() => {
- debouncedRefreshModels()
- return () => debouncedRefreshModels.clear()
- })
-
useEffect(() => {
- if (!refreshValues) {
- prevRefreshValuesRef.current = undefined
- return
+ if (apiConfiguration[modelIdKey] == null && defaultModelId) {
+ onSelect(defaultModelId)
}
-
- // Check if all values in refreshValues are truthy
- if (Object.values(refreshValues).some((value) => !value)) {
- prevRefreshValuesRef.current = undefined
- return
- }
-
- // Compare with previous values
- const prevValues = prevRefreshValuesRef.current
- if (prevValues && JSON.stringify(prevValues) === JSON.stringify(refreshValues)) {
- return
- }
-
- prevRefreshValuesRef.current = refreshValues
- debouncedRefreshModels()
- }, [debouncedRefreshModels, refreshValues])
-
- useEffect(() => setValue(selectedModelId), [selectedModelId])
+ }, [apiConfiguration, defaultModelId, modelIdKey, onSelect])
return (
<>
Model
-
-
-
-
-
-
-
-
- No model found.
-
- {modelIds.map((model) => (
-
- {model}
-
-
- ))}
-
- {allowCustomModel && (
-
- {
- setIsCustomModel(true)
- setOpen(false)
- }}>
- + Add custom model
-
-
- )}
-
-
-
-
+
+
+
+ No model found.
+ {modelIds.map((model) => (
+
+ {model}
+
+ ))}
+
+
+
{selectedModelId && selectedModelInfo && (
onSelect(recommendedModel)}>{recommendedModel}.
You can also try searching "free" for no-cost options currently available.
- {allowCustomModel && isCustomModel && (
-
-
-
Add Custom Model
-
setCustomModelId(e.target.value)}
- />
-
-
-
-
-
-
- )}
>
)
}
diff --git a/webview-ui/src/components/settings/OpenAiModelPicker.tsx b/webview-ui/src/components/settings/OpenAiModelPicker.tsx
deleted file mode 100644
index 040da1d421..0000000000
--- a/webview-ui/src/components/settings/OpenAiModelPicker.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from "react"
-import { useExtensionState } from "../../context/ExtensionStateContext"
-import { ModelPicker } from "./ModelPicker"
-
-const OpenAiModelPicker: React.FC = () => {
- const { apiConfiguration } = useExtensionState()
-
- return (
-
- )
-}
-
-export default OpenAiModelPicker
diff --git a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx b/webview-ui/src/components/settings/OpenRouterModelPicker.tsx
deleted file mode 100644
index c773478e54..0000000000
--- a/webview-ui/src/components/settings/OpenRouterModelPicker.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ModelPicker } from "./ModelPicker"
-import { openRouterDefaultModelId } from "../../../../src/shared/api"
-
-export const OpenRouterModelPicker = () => (
-
-)
diff --git a/webview-ui/src/components/settings/RequestyModelPicker.tsx b/webview-ui/src/components/settings/RequestyModelPicker.tsx
deleted file mode 100644
index c65067068a..0000000000
--- a/webview-ui/src/components/settings/RequestyModelPicker.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { ModelPicker } from "./ModelPicker"
-import { requestyDefaultModelId } from "../../../../src/shared/api"
-import { useExtensionState } from "@/context/ExtensionStateContext"
-
-export const RequestyModelPicker = () => {
- const { apiConfiguration } = useExtensionState()
- return (
-
- )
-}
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index 761e856521..75ba11107c 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -1,4 +1,4 @@
-import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"
+import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { Dropdown, type DropdownOption } from "vscrui"
@@ -45,7 +45,6 @@ const SettingsView = forwardRef(({ onDone },
// TODO: Reduce WebviewMessage/ExtensionState complexity
const { currentApiConfigName } = extensionState
const {
- apiConfiguration,
alwaysAllowReadOnly,
allowedCommands,
alwaysAllowBrowser,
@@ -69,6 +68,9 @@ const SettingsView = forwardRef(({ onDone },
terminalOutputLineLimit,
writeDelayMs,
} = cachedState
+
+ //Make sure apiConfiguration is initialized and managed by SettingsView
+ const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
useEffect(() => {
// Update only when currentApiConfigName is changed
diff --git a/webview-ui/src/components/settings/UnboundModelPicker.tsx b/webview-ui/src/components/settings/UnboundModelPicker.tsx
deleted file mode 100644
index 4901884f1e..0000000000
--- a/webview-ui/src/components/settings/UnboundModelPicker.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ModelPicker } from "./ModelPicker"
-import { unboundDefaultModelId } from "../../../../src/shared/api"
-
-export const UnboundModelPicker = () => (
-
-)
diff --git a/webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx b/webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx
index 4e7c67c187..49d60c55c4 100644
--- a/webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ModelPicker.test.tsx
@@ -3,7 +3,6 @@
import { screen, fireEvent, render } from "@testing-library/react"
import { act } from "react"
import { ModelPicker } from "../ModelPicker"
-import { useExtensionState } from "../../../context/ExtensionStateContext"
jest.mock("../../../context/ExtensionStateContext", () => ({
useExtensionState: jest.fn(),
@@ -20,36 +19,40 @@ global.ResizeObserver = MockResizeObserver
Element.prototype.scrollIntoView = jest.fn()
describe("ModelPicker", () => {
- const mockOnUpdateApiConfig = jest.fn()
- const mockSetApiConfiguration = jest.fn()
-
+ const mockSetApiConfigurationField = jest.fn()
+ const modelInfo = {
+ maxTokens: 8192,
+ contextWindow: 200_000,
+ supportsImages: true,
+ supportsComputerUse: true,
+ supportsPromptCache: true,
+ inputPrice: 3.0,
+ outputPrice: 15.0,
+ cacheWritesPrice: 3.75,
+ cacheReadsPrice: 0.3,
+ }
+ const mockModels = {
+ model1: { name: "Model 1", description: "Test model 1", ...modelInfo },
+ model2: { name: "Model 2", description: "Test model 2", ...modelInfo },
+ }
const defaultProps = {
+ apiConfiguration: {},
defaultModelId: "model1",
- modelsKey: "glamaModels" as const,
- configKey: "glamaModelId" as const,
- infoKey: "glamaModelInfo" as const,
- refreshMessageType: "refreshGlamaModels" as const,
+ defaultModelInfo: modelInfo,
+ modelIdKey: "glamaModelId" as const,
+ modelInfoKey: "glamaModelInfo" as const,
serviceName: "Test Service",
serviceUrl: "https://test.service",
recommendedModel: "recommended-model",
- }
-
- const mockModels = {
- model1: { name: "Model 1", description: "Test model 1" },
- model2: { name: "Model 2", description: "Test model 2" },
+ models: mockModels,
+ setApiConfigurationField: mockSetApiConfigurationField,
}
beforeEach(() => {
jest.clearAllMocks()
- ;(useExtensionState as jest.Mock).mockReturnValue({
- apiConfiguration: {},
- setApiConfiguration: mockSetApiConfiguration,
- glamaModels: mockModels,
- onUpdateApiConfig: mockOnUpdateApiConfig,
- })
})
- it("calls onUpdateApiConfig when a model is selected", async () => {
+ it("calls setApiConfigurationField when a model is selected", async () => {
await act(async () => {
render()
})
@@ -67,20 +70,12 @@ describe("ModelPicker", () => {
await act(async () => {
// Find and click the model item by its value.
- const modelItem = screen.getByRole("option", { name: "model2" })
- fireEvent.click(modelItem)
+ const modelItem = screen.getByTestId("model-input")
+ fireEvent.input(modelItem, { target: { value: "model2" } })
})
// Verify the API config was updated.
- expect(mockSetApiConfiguration).toHaveBeenCalledWith({
- glamaModelId: "model2",
- glamaModelInfo: mockModels["model2"],
- })
-
- // Verify onUpdateApiConfig was called with the new config.
- expect(mockOnUpdateApiConfig).toHaveBeenCalledWith({
- glamaModelId: "model2",
- glamaModelInfo: mockModels["model2"],
- })
+ expect(mockSetApiConfigurationField).toHaveBeenCalledWith(defaultProps.modelIdKey, "model2")
+ expect(mockSetApiConfigurationField).toHaveBeenCalledWith(defaultProps.modelInfoKey, mockModels.model2)
})
})
diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts
index 19b13e2c6c..97c702637c 100644
--- a/webview-ui/src/utils/validate.ts
+++ b/webview-ui/src/utils/validate.ts
@@ -1,9 +1,4 @@
-import {
- ApiConfiguration,
- glamaDefaultModelId,
- openRouterDefaultModelId,
- unboundDefaultModelId,
-} from "../../../src/shared/api"
+import { ApiConfiguration } from "../../../src/shared/api"
import { ModelInfo } from "../../../src/shared/api"
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
if (apiConfiguration) {
@@ -86,7 +81,7 @@ export function validateModelId(
if (apiConfiguration) {
switch (apiConfiguration.apiProvider) {
case "glama":
- const glamaModelId = apiConfiguration.glamaModelId || glamaDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
+ const glamaModelId = apiConfiguration.glamaModelId
if (!glamaModelId) {
return "You must provide a model ID."
}
@@ -96,7 +91,7 @@ export function validateModelId(
}
break
case "openrouter":
- const modelId = apiConfiguration.openRouterModelId || openRouterDefaultModelId // in case the user hasn't changed the model id, it will be undefined by default
+ const modelId = apiConfiguration.openRouterModelId
if (!modelId) {
return "You must provide a model ID."
}
@@ -106,7 +101,7 @@ export function validateModelId(
}
break
case "unbound":
- const unboundModelId = apiConfiguration.unboundModelId || unboundDefaultModelId
+ const unboundModelId = apiConfiguration.unboundModelId
if (!unboundModelId) {
return "You must provide a model ID."
}
From 1a3b8700ba2c8f93f31cdfed7aa0fadb38d584d0 Mon Sep 17 00:00:00 2001
From: System233
Date: Wed, 26 Feb 2025 06:42:29 +0800
Subject: [PATCH 22/32] Improved error message feedback in settings panel
---
webview-ui/src/__mocks__/vscrui.ts | 3 +
.../components/settings/ApiErrorMessage.tsx | 16 +++++
.../src/components/settings/ApiOptions.tsx | 69 ++++++++-----------
.../src/components/settings/ModelPicker.tsx | 41 ++++++++---
.../src/components/settings/SettingsView.tsx | 53 ++++----------
.../settings/__tests__/ApiOptions.test.tsx | 4 ++
.../src/components/welcome/WelcomeView.tsx | 2 +
7 files changed, 98 insertions(+), 90 deletions(-)
create mode 100644 webview-ui/src/components/settings/ApiErrorMessage.tsx
diff --git a/webview-ui/src/__mocks__/vscrui.ts b/webview-ui/src/__mocks__/vscrui.ts
index 76760ba5cc..9b4a20f4d6 100644
--- a/webview-ui/src/__mocks__/vscrui.ts
+++ b/webview-ui/src/__mocks__/vscrui.ts
@@ -8,6 +8,9 @@ export const Dropdown = ({ children, value, onChange }: any) =>
export const Pane = ({ children }: any) => React.createElement("div", { "data-testid": "mock-pane" }, children)
+export const Button = ({ children, ...props }: any) =>
+ React.createElement("div", { "data-testid": "mock-button", ...props }, children)
+
export type DropdownOption = {
label: string
value: string
diff --git a/webview-ui/src/components/settings/ApiErrorMessage.tsx b/webview-ui/src/components/settings/ApiErrorMessage.tsx
new file mode 100644
index 0000000000..4b419957b6
--- /dev/null
+++ b/webview-ui/src/components/settings/ApiErrorMessage.tsx
@@ -0,0 +1,16 @@
+import React from "react"
+
+interface ApiErrorMessageProps {
+ errorMessage: string | undefined
+ children?: React.ReactNode
+}
+const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => {
+ return (
+
+
+ {errorMessage}
+ {children}
+
+ )
+}
+export default ApiErrorMessage
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 594cd2fd5f..f0c2b0e45f 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -1,4 +1,4 @@
-import { memo, useCallback, useMemo, useState } from "react"
+import React, { memo, useCallback, useEffect, useMemo, useState } from "react"
import { useDebounce, useEvent } from "react-use"
import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui"
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
@@ -42,23 +42,25 @@ import { ModelInfoView } from "./ModelInfoView"
import { DROPDOWN_Z_INDEX } from "./styles"
import { ModelPicker } from "./ModelPicker"
import { TemperatureControl } from "./TemperatureControl"
+import { validateApiConfiguration, validateModelId } from "@/utils/validate"
+import ApiErrorMessage from "./ApiErrorMessage"
interface ApiOptionsProps {
uriScheme: string | undefined
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void
- apiErrorMessage?: string
- modelIdErrorMessage?: string
fromWelcomeView?: boolean
+ errorMessage: string | undefined
+ setErrorMessage: React.Dispatch>
}
const ApiOptions = ({
uriScheme,
apiConfiguration,
setApiConfigurationField,
- apiErrorMessage,
- modelIdErrorMessage,
fromWelcomeView,
+ errorMessage,
+ setErrorMessage,
}: ApiOptionsProps) => {
const [ollamaModels, setOllamaModels] = useState([])
const [lmStudioModels, setLmStudioModels] = useState([])
@@ -146,6 +148,13 @@ const ApiOptions = ({
],
)
+ useEffect(() => {
+ const apiValidationResult =
+ validateApiConfiguration(apiConfiguration) ||
+ validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels)
+ setErrorMessage(apiValidationResult)
+ }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels])
+
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
@@ -626,6 +635,7 @@ const ApiOptions = ({
]}
/>
+ {errorMessage && }
{/* end Model Info Configuration */}
-
-
-
- (Note: Roo Code uses complex prompts and works best
- with Claude models. Less capable models may not work as expected.)
-
-
)}
@@ -1100,6 +1099,7 @@ const ApiOptions = ({
placeholder={"e.g. meta-llama-3.1-8b-instruct"}>
Model ID
+ {errorMessage && }
{lmStudioModels.length > 0 && (
Model ID
+ {errorMessage && (
+
+
+ {errorMessage}
+
+ )}
{ollamaModels.length > 0 && (
)}
- {apiErrorMessage && (
-
-
- {apiErrorMessage}
-
- )}
-
{selectedProvider === "glama" && (
)}
@@ -1364,6 +1360,7 @@ const ApiOptions = ({
serviceName="OpenRouter"
serviceUrl="https://openrouter.ai/models"
recommendedModel="anthropic/claude-3.7-sonnet"
+ errorMessage={errorMessage}
/>
)}
{selectedProvider === "requesty" && (
@@ -1378,6 +1375,7 @@ const ApiOptions = ({
serviceName="Requesty"
serviceUrl="https://requesty.ai"
recommendedModel="anthropic/claude-3-7-sonnet-latest"
+ errorMessage={errorMessage}
/>
)}
@@ -1401,6 +1399,7 @@ const ApiOptions = ({
{selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
{selectedProvider === "mistral" && createDropdown(mistralModels)}
+ {errorMessage && }
)}
-
- {modelIdErrorMessage && (
-
-
- {modelIdErrorMessage}
-
- )}
)
}
diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx
index 8fd6d82daa..fd62bfb97b 100644
--- a/webview-ui/src/components/settings/ModelPicker.tsx
+++ b/webview-ui/src/components/settings/ModelPicker.tsx
@@ -5,6 +5,7 @@ import { normalizeApiConfiguration } from "./ApiOptions"
import { ModelInfoView } from "./ModelInfoView"
import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "../ui/combobox"
+import ApiErrorMessage from "./ApiErrorMessage"
type ExtractType = NonNullable<
{ [K in keyof ApiConfiguration]: Required[K] extends T ? K : never }[keyof ApiConfiguration]
@@ -30,6 +31,7 @@ interface ModelPickerProps {
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void
defaultModelInfo?: ModelInfo
+ errorMessage?: string
}
export const ModelPicker = ({
@@ -43,6 +45,7 @@ export const ModelPicker = ({
apiConfiguration,
setApiConfigurationField,
defaultModelInfo,
+ errorMessage,
}: ModelPickerProps) => {
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
@@ -69,11 +72,16 @@ export const ModelPicker = ({
return (
<>
Model
-
+
No model found.
@@ -85,13 +93,30 @@ export const ModelPicker = ({
- {selectedModelId && selectedModelInfo && (
-
+ {errorMessage ? (
+
+
+
+ Note: Roo Code uses complex prompts and works best
+ with Claude models. Less capable models may not work as expected.
+
+
+
+ ) : (
+ selectedModelId &&
+ selectedModelInfo && (
+
+ )
)}
The extension automatically fetches the latest list of models available on{" "}
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index 75ba11107c..ee032c3ee0 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -1,6 +1,6 @@
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-import { Dropdown, type DropdownOption } from "vscrui"
+import { Button, Dropdown, type DropdownOption } from "vscrui"
import {
AlertDialog,
@@ -14,7 +14,6 @@ import {
} from "@/components/ui"
import { vscode } from "../../utils/vscode"
-import { validateApiConfiguration, validateModelId } from "../../utils/validate"
import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext"
import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
import { ApiConfiguration } from "../../../../src/shared/api"
@@ -33,14 +32,13 @@ export interface SettingsViewRef {
const SettingsView = forwardRef(({ onDone }, ref) => {
const extensionState = useExtensionState()
- const [apiErrorMessage, setApiErrorMessage] = useState(undefined)
- const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined)
const [commandInput, setCommandInput] = useState("")
const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
const [cachedState, setCachedState] = useState(extensionState)
const [isChangeDetected, setChangeDetected] = useState(false)
const prevApiConfigName = useRef(extensionState.currentApiConfigName)
const confirmDialogHandler = useRef<() => void>()
+ const [errorMessage, setErrorMessage] = useState(undefined)
// TODO: Reduce WebviewMessage/ExtensionState complexity
const { currentApiConfigName } = extensionState
@@ -135,20 +133,9 @@ const SettingsView = forwardRef(({ onDone },
}
})
}, [])
-
+ const isSettingValid = !errorMessage
const handleSubmit = () => {
- const apiValidationResult = validateApiConfiguration(apiConfiguration)
-
- const modelIdValidationResult = validateModelId(
- apiConfiguration,
- extensionState.glamaModels,
- extensionState.openRouterModels,
- )
-
- setApiErrorMessage(apiValidationResult)
- setModelIdErrorMessage(modelIdValidationResult)
-
- if (!apiValidationResult && !modelIdValidationResult) {
+ if (isSettingValid) {
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
@@ -177,23 +164,6 @@ const SettingsView = forwardRef(({ onDone },
}
}
- useEffect(() => {
- setApiErrorMessage(undefined)
- setModelIdErrorMessage(undefined)
- }, [apiConfiguration])
-
- // Initial validation on mount
- useEffect(() => {
- const apiValidationResult = validateApiConfiguration(apiConfiguration)
- const modelIdValidationResult = validateModelId(
- apiConfiguration,
- extensionState.glamaModels,
- extensionState.openRouterModels,
- )
- setApiErrorMessage(apiValidationResult)
- setModelIdErrorMessage(modelIdValidationResult)
- }, [apiConfiguration, extensionState.glamaModels, extensionState.openRouterModels])
-
const checkUnsaveChanges = useCallback(
(then: () => void) => {
if (isChangeDetected) {
@@ -287,13 +257,14 @@ const SettingsView = forwardRef(({ onDone },
justifyContent: "space-between",
gap: "6px",
}}>
-
+ disabled={!isChangeDetected || !isSettingValid}>
Save
-
+
(({ onDone },
uriScheme={extensionState.uriScheme}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
- apiErrorMessage={apiErrorMessage}
- modelIdErrorMessage={modelIdErrorMessage}
+ errorMessage={errorMessage}
+ setErrorMessage={setErrorMessage}
/>
diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
index 8f2d0dff89..73394bae10 100644
--- a/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/ApiOptions.test.tsx
@@ -51,6 +51,8 @@ describe("ApiOptions", () => {
render(
{}}
uriScheme={undefined}
apiConfiguration={{}}
setApiConfigurationField={() => {}}
@@ -69,4 +71,6 @@ describe("ApiOptions", () => {
renderApiOptions({ fromWelcomeView: true })
expect(screen.queryByTestId("temperature-control")).not.toBeInTheDocument()
})
+
+ //TODO: More test cases needed
})
diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx
index 858d2622f3..5d880efc0b 100644
--- a/webview-ui/src/components/welcome/WelcomeView.tsx
+++ b/webview-ui/src/components/welcome/WelcomeView.tsx
@@ -42,6 +42,8 @@ const WelcomeView = () => {
apiConfiguration={apiConfiguration || {}}
uriScheme={uriScheme}
setApiConfigurationField={(field, value) => setApiConfiguration({ [field]: value })}
+ errorMessage={errorMessage}
+ setErrorMessage={setErrorMessage}
/>
From 48975003afe593c975476acfd348ceb6110d7ced Mon Sep 17 00:00:00 2001
From: System233
Date: Wed, 26 Feb 2025 06:50:11 +0800
Subject: [PATCH 23/32] Remove ModelInfo related exports from
ExtensionStateContext
---
.../src/context/ExtensionStateContext.tsx | 84 +------------------
1 file changed, 1 insertion(+), 83 deletions(-)
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx
index 3dca8d5f51..c2c4d181e4 100644
--- a/webview-ui/src/context/ExtensionStateContext.tsx
+++ b/webview-ui/src/context/ExtensionStateContext.tsx
@@ -1,18 +1,7 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
import { useEvent } from "react-use"
import { ApiConfigMeta, ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
-import {
- ApiConfiguration,
- ModelInfo,
- glamaDefaultModelId,
- glamaDefaultModelInfo,
- openRouterDefaultModelId,
- openRouterDefaultModelInfo,
- unboundDefaultModelId,
- unboundDefaultModelInfo,
- requestyDefaultModelId,
- requestyDefaultModelInfo,
-} from "../../../src/shared/api"
+import { ApiConfiguration } from "../../../src/shared/api"
import { vscode } from "../utils/vscode"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { findLastIndex } from "../../../src/shared/array"
@@ -26,11 +15,6 @@ export interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
showWelcome: boolean
theme: any
- glamaModels: Record
- requestyModels: Record
- openRouterModels: Record
- unboundModels: Record
- openAiModels: string[]
mcpServers: McpServer[]
currentCheckpoint?: string
filePaths: string[]
@@ -70,7 +54,6 @@ export interface ExtensionStateContextType extends ExtensionState {
setRateLimitSeconds: (value: number) => void
setCurrentApiConfigName: (value: string) => void
setListApiConfigMeta: (value: ApiConfigMeta[]) => void
- onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
mode: Mode
setMode: (value: Mode) => void
setCustomModePrompts: (value: CustomModePrompts) => void
@@ -124,21 +107,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
const [showWelcome, setShowWelcome] = useState(false)
const [theme, setTheme] = useState(undefined)
const [filePaths, setFilePaths] = useState([])
- const [glamaModels, setGlamaModels] = useState>({
- [glamaDefaultModelId]: glamaDefaultModelInfo,
- })
const [openedTabs, setOpenedTabs] = useState>([])
- const [openRouterModels, setOpenRouterModels] = useState>({
- [openRouterDefaultModelId]: openRouterDefaultModelInfo,
- })
- const [unboundModels, setUnboundModels] = useState>({
- [unboundDefaultModelId]: unboundDefaultModelInfo,
- })
- const [requestyModels, setRequestyModels] = useState>({
- [requestyDefaultModelId]: requestyDefaultModelInfo,
- })
- const [openAiModels, setOpenAiModels] = useState([])
const [mcpServers, setMcpServers] = useState([])
const [currentCheckpoint, setCurrentCheckpoint] = useState()
@@ -146,18 +116,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
(value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
[],
)
-
- const onUpdateApiConfig = useCallback((apiConfig: ApiConfiguration) => {
- setState((currentState) => {
- vscode.postMessage({
- type: "upsertApiConfiguration",
- text: currentState.currentApiConfigName,
- apiConfiguration: { ...currentState.apiConfiguration, ...apiConfig },
- })
- return currentState // No state update needed
- })
- }, [])
-
const handleMessage = useCallback(
(event: MessageEvent) => {
const message: ExtensionMessage = event.data
@@ -202,40 +160,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
})
break
}
- case "glamaModels": {
- const updatedModels = message.glamaModels ?? {}
- setGlamaModels({
- [glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
- case "openRouterModels": {
- const updatedModels = message.openRouterModels ?? {}
- setOpenRouterModels({
- [openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
- case "openAiModels": {
- const updatedModels = message.openAiModels ?? []
- setOpenAiModels(updatedModels)
- break
- }
- case "unboundModels": {
- const updatedModels = message.unboundModels ?? {}
- setUnboundModels(updatedModels)
- break
- }
- case "requestyModels": {
- const updatedModels = message.requestyModels ?? {}
- setRequestyModels({
- [requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
case "mcpServers": {
setMcpServers(message.mcpServers ?? [])
break
@@ -264,11 +188,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
didHydrateState,
showWelcome,
theme,
- glamaModels,
- requestyModels,
- openRouterModels,
- openAiModels,
- unboundModels,
mcpServers,
currentCheckpoint,
filePaths,
@@ -316,7 +235,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })),
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
setListApiConfigMeta,
- onUpdateApiConfig,
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
setCustomModePrompts: (value) => setState((prevState) => ({ ...prevState, customModePrompts: value })),
setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })),
From 05151ed2254e8698d4c380ceb8daa36e019f82aa Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Tue, 25 Feb 2025 18:30:37 -0500
Subject: [PATCH 24/32] Update package.json
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 84bec2645a..28045436e6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "roo-cline",
"displayName": "Roo Code (prev. Roo Cline)",
- "description": "An AI-powered autonomous coding agent that lives in your editor.",
+ "description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
"version": "3.7.4",
"icon": "assets/icons/rocket.png",
From e56908f6c1b800bd2a2b4edd85f725c1a0055920 Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 15:48:20 -0800
Subject: [PATCH 25/32] Thinking settings tweaks
---
src/api/providers/anthropic.ts | 23 +++++----
src/api/providers/openrouter.ts | 12 ++++-
src/shared/api.ts | 14 +++++-
.../src/components/settings/ApiOptions.tsx | 50 ++++++-------------
4 files changed, 51 insertions(+), 48 deletions(-)
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index 2d1f07f833..c907350607 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -14,8 +14,6 @@ import { ApiStream } from "../transform/stream"
const ANTHROPIC_DEFAULT_TEMPERATURE = 0
-const THINKING_MODELS = ["claude-3-7-sonnet-20250219"]
-
export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
private options: ApiHandlerOptions
private client: Anthropic
@@ -32,16 +30,19 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
let stream: AnthropicStream
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
- const modelId = this.getModel().id
- const maxTokens = this.getModel().info.maxTokens || 8192
+ let { id: modelId, info: modelInfo } = this.getModel()
+ const maxTokens = modelInfo.maxTokens || 8192
+ const budgetTokens = this.options.anthropicThinking ?? Math.min(maxTokens - 1, 8192)
let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
let thinking: BetaThinkingConfigParam | undefined = undefined
- if (THINKING_MODELS.includes(modelId)) {
- thinking = this.options.anthropicThinking
- ? { type: "enabled", budget_tokens: this.options.anthropicThinking }
- : { type: "disabled" }
-
+ // Anthropic "Thinking" models require a temperature of 1.0.
+ if (modelId === "claude-3-7-sonnet-20250219:thinking") {
+ // The `:thinking` variant is a virtual identifier for the
+ // `claude-3-7-sonnet-20250219` model with a thinking budget.
+ // We can handle this more elegantly in the future.
+ modelId = "claude-3-7-sonnet-20250219"
+ thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
@@ -114,8 +115,8 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
default: {
stream = (await this.client.messages.create({
model: modelId,
- max_tokens: this.getModel().info.maxTokens || 8192,
- temperature: this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE,
+ max_tokens: maxTokens,
+ temperature,
system: [{ text: systemPrompt, type: "text" }],
messages,
// tools,
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 69c55b8e71..6bf4fa4a8c 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -1,4 +1,5 @@
import { Anthropic } from "@anthropic-ai/sdk"
+import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta"
import axios from "axios"
import OpenAI from "openai"
import delay from "delay"
@@ -17,6 +18,7 @@ const OPENROUTER_DEFAULT_TEMPERATURE = 0
type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
transforms?: string[]
include_reasoning?: boolean
+ thinking?: BetaThinkingConfigParam
}
// Add custom interface for OpenRouter usage chunk.
@@ -57,7 +59,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
// prompt caching: https://openrouter.ai/docs/prompt-caching
// this is specifically for claude models (some models may 'support prompt caching' automatically without this)
switch (true) {
- case this.getModel().id.startsWith("anthropic/"):
+ case modelId.startsWith("anthropic/"):
openAiMessages[0] = {
role: "system",
content: [
@@ -108,8 +110,13 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
let temperature = this.options.modelTemperature ?? defaultTemperature
+ const maxTokens = modelInfo.maxTokens
+ const budgetTokens = this.options.anthropicThinking ?? Math.min((maxTokens ?? 8192) - 1, 8192)
+ let thinking: BetaThinkingConfigParam | undefined = undefined
+
// Anthropic "Thinking" models require a temperature of 1.0.
if (modelInfo.thinking) {
+ thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
@@ -118,8 +125,9 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
const completionParams: OpenRouterChatCompletionParams = {
model: modelId,
- max_tokens: modelInfo.maxTokens,
+ max_tokens: maxTokens,
temperature,
+ thinking, // OpenRouter is temporarily supporting this.
top_p: topP,
messages: openAiMessages,
stream: true,
diff --git a/src/shared/api.ts b/src/shared/api.ts
index 63707e52b4..5d4b8b120d 100644
--- a/src/shared/api.ts
+++ b/src/shared/api.ts
@@ -103,7 +103,7 @@ export const THINKING_BUDGET = {
export type AnthropicModelId = keyof typeof anthropicModels
export const anthropicDefaultModelId: AnthropicModelId = "claude-3-7-sonnet-20250219"
export const anthropicModels = {
- "claude-3-7-sonnet-20250219": {
+ "claude-3-7-sonnet-20250219:thinking": {
maxTokens: 16384,
contextWindow: 200_000,
supportsImages: true,
@@ -115,6 +115,18 @@ export const anthropicModels = {
cacheReadsPrice: 0.3, // $0.30 per million tokens
thinking: true,
},
+ "claude-3-7-sonnet-20250219": {
+ maxTokens: 16384,
+ contextWindow: 200_000,
+ supportsImages: true,
+ supportsComputerUse: true,
+ supportsPromptCache: true,
+ inputPrice: 3.0, // $3 per million input tokens
+ outputPrice: 15.0, // $15 per million output tokens
+ cacheWritesPrice: 3.75, // $3.75 per million tokens
+ cacheReadsPrice: 0.3, // $0.30 per million tokens
+ thinking: false,
+ },
"claude-3-5-sonnet-20241022": {
maxTokens: 8192,
contextWindow: 200_000,
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 9d17cae4fa..73dc4fd41f 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -73,7 +73,7 @@ const ApiOptions = ({
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
- const anthropicThinkingBudget = apiConfiguration?.anthropicThinking
+ const anthropicThinkingBudget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
const noTransform = (value: T) => value
const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any
@@ -1272,39 +1272,21 @@ const ApiOptions = ({
)}
{selectedModelInfo && selectedModelInfo.thinking && (
-
-
- setApiConfigurationField(
- "anthropicThinking",
- checked
- ? Math.min(
- THINKING_BUDGET.default,
- selectedModelInfo.maxTokens ?? THINKING_BUDGET.default,
- )
- : undefined,
- )
- }>
- Thinking?
-
- {anthropicThinkingBudget && (
- <>
-
- Number of tokens Claude is allowed to use for its internal reasoning process.
-
-
-
setApiConfigurationField("anthropicThinking", value[0])}
- />
- {anthropicThinkingBudget}
-
- >
- )}
+
+
Thinking Budget
+
+
setApiConfigurationField("anthropicThinking", value[0])}
+ />
+ {anthropicThinkingBudget}
+
+
+ Number of tokens Claude is allowed to use for its internal reasoning process.
+
)}
From 8971e47b96ec7ae3a6de4ec5a95a2acab4cba1b7 Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 15:53:32 -0800
Subject: [PATCH 26/32] Add changeset
---
.changeset/swift-kings-attack.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/swift-kings-attack.md
diff --git a/.changeset/swift-kings-attack.md b/.changeset/swift-kings-attack.md
new file mode 100644
index 0000000000..8a8a425611
--- /dev/null
+++ b/.changeset/swift-kings-attack.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Pass "thinking" params to OpenRouter
From 33fd3bd6b35caafce66fcd53b9070f60279fcc0d Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 16:26:40 -0800
Subject: [PATCH 27/32] Fix budgetTokens
---
src/api/providers/anthropic.ts | 2 +-
src/api/providers/openrouter.ts | 8 +++-----
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts
index c907350607..ad58a1cf6b 100644
--- a/src/api/providers/anthropic.ts
+++ b/src/api/providers/anthropic.ts
@@ -32,7 +32,6 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
let { id: modelId, info: modelInfo } = this.getModel()
const maxTokens = modelInfo.maxTokens || 8192
- const budgetTokens = this.options.anthropicThinking ?? Math.min(maxTokens - 1, 8192)
let temperature = this.options.modelTemperature ?? ANTHROPIC_DEFAULT_TEMPERATURE
let thinking: BetaThinkingConfigParam | undefined = undefined
@@ -42,6 +41,7 @@ export class AnthropicHandler implements ApiHandler, SingleCompletionHandler {
// `claude-3-7-sonnet-20250219` model with a thinking budget.
// We can handle this more elegantly in the future.
modelId = "claude-3-7-sonnet-20250219"
+ const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024)
thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts
index 6bf4fa4a8c..0a9488e816 100644
--- a/src/api/providers/openrouter.ts
+++ b/src/api/providers/openrouter.ts
@@ -109,13 +109,11 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
}
let temperature = this.options.modelTemperature ?? defaultTemperature
-
- const maxTokens = modelInfo.maxTokens
- const budgetTokens = this.options.anthropicThinking ?? Math.min((maxTokens ?? 8192) - 1, 8192)
let thinking: BetaThinkingConfigParam | undefined = undefined
- // Anthropic "Thinking" models require a temperature of 1.0.
if (modelInfo.thinking) {
+ const maxTokens = modelInfo.maxTokens || 8192
+ const budgetTokens = this.options.anthropicThinking ?? Math.max(maxTokens * 0.8, 1024)
thinking = { type: "enabled", budget_tokens: budgetTokens }
temperature = 1.0
}
@@ -125,7 +123,7 @@ export class OpenRouterHandler implements ApiHandler, SingleCompletionHandler {
const completionParams: OpenRouterChatCompletionParams = {
model: modelId,
- max_tokens: maxTokens,
+ max_tokens: modelInfo.maxTokens,
temperature,
thinking, // OpenRouter is temporarily supporting this.
top_p: topP,
From f3d02030ac47420d8a9735c0d02f70e561475dae Mon Sep 17 00:00:00 2001
From: System233
Date: Wed, 26 Feb 2025 08:37:28 +0800
Subject: [PATCH 28/32] Fix: Input/output prices should be parsed using
parseFloat
---
webview-ui/src/components/settings/ApiOptions.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 73dc4fd41f..bfcf93256e 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -900,7 +900,7 @@ const ApiOptions = ({
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
- const parsed = parseInt(value)
+ const parsed = parseFloat(value)
return {
...(apiConfiguration?.openAiCustomModelInfo ??
openAiModelInfoSaneDefaults),
@@ -945,7 +945,7 @@ const ApiOptions = ({
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
- const parsed = parseInt(value)
+ const parsed = parseFloat(value)
return {
...(apiConfiguration?.openAiCustomModelInfo ||
openAiModelInfoSaneDefaults),
From 41e75bc9890036674cd31ce2d9da23fcd5127956 Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 23:02:43 -0800
Subject: [PATCH 29/32] Model picker fixes
---
.changeset/real-ties-destroy.md | 5 +
src/api/providers/requesty.ts | 36 +-
src/core/webview/ClineProvider.ts | 22 +-
src/shared/ExtensionMessage.ts | 13 +-
src/shared/WebviewMessage.ts | 5 +-
webview-ui/package-lock.json | 2 +
.../components/settings/ApiErrorMessage.tsx | 18 +-
.../src/components/settings/ApiOptions.tsx | 445 ++++++++----------
.../src/components/settings/ModelPicker.tsx | 91 ++--
.../src/components/settings/SettingsView.tsx | 48 +-
.../components/settings/ThinkingBudget.tsx | 29 ++
webview-ui/src/components/ui/alert-dialog.tsx | 151 +++---
webview-ui/src/components/ui/dialog.tsx | 158 ++++---
.../src/components/welcome/WelcomeView.tsx | 8 +-
webview-ui/src/utils/validate.ts | 254 ++++++----
15 files changed, 664 insertions(+), 621 deletions(-)
create mode 100644 .changeset/real-ties-destroy.md
create mode 100644 webview-ui/src/components/settings/ThinkingBudget.tsx
diff --git a/.changeset/real-ties-destroy.md b/.changeset/real-ties-destroy.md
new file mode 100644
index 0000000000..a2e9ba8eb0
--- /dev/null
+++ b/.changeset/real-ties-destroy.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Fix model picker
diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts
index 2151a7172d..5e570ca2a2 100644
--- a/src/api/providers/requesty.ts
+++ b/src/api/providers/requesty.ts
@@ -42,26 +42,33 @@ export class RequestyHandler extends OpenAiHandler {
}
}
-export async function getRequestyModels({ apiKey }: { apiKey?: string }) {
+export async function getRequestyModels() {
const models: Record = {}
- if (!apiKey) {
- return models
- }
-
try {
- const config: Record = {}
- config["headers"] = { Authorization: `Bearer ${apiKey}` }
-
- const response = await axios.get("https://router.requesty.ai/v1/models", config)
+ const response = await axios.get("https://router.requesty.ai/v1/models")
const rawModels = response.data.data
for (const rawModel of rawModels) {
+ // {
+ // id: "anthropic/claude-3-5-sonnet-20240620",
+ // object: "model",
+ // created: 1740552655,
+ // owned_by: "system",
+ // input_price: 0.0000028,
+ // caching_price: 0.00000375,
+ // cached_price: 3e-7,
+ // output_price: 0.000015,
+ // max_output_tokens: 8192,
+ // context_window: 200000,
+ // supports_caching: true,
+ // description:
+ // "Anthropic's previous most intelligent model. High level of intelligence and capability. Excells in coding.",
+ // }
+
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),
@@ -72,8 +79,15 @@ export async function getRequestyModels({ apiKey }: { apiKey?: string }) {
switch (rawModel.id) {
case rawModel.id.startsWith("anthropic/claude-3-7-sonnet"):
+ modelInfo.supportsComputerUse = true
+ modelInfo.supportsImages = true
modelInfo.maxTokens = 16384
break
+ case rawModel.id.startsWith("anthropic/claude-3-5-sonnet-20241022"):
+ modelInfo.supportsComputerUse = true
+ modelInfo.supportsImages = true
+ modelInfo.maxTokens = 8192
+ break
case rawModel.id.startsWith("anthropic/"):
modelInfo.maxTokens = 8192
break
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 118bbddcf5..bc6f457868 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -644,9 +644,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
})
- const requestyApiKey = await this.getSecret("requestyApiKey")
-
- getRequestyModels({ apiKey: requestyApiKey }).then(async (requestyModels) => {
+ getRequestyModels().then(async (requestyModels) => {
if (Object.keys(requestyModels).length > 0) {
await fs.writeFile(
path.join(cacheDir, GlobalFileNames.requestyModels),
@@ -838,17 +836,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
break
case "refreshRequestyModels":
- if (message?.values?.apiKey) {
- const requestyModels = await getRequestyModels({ apiKey: message.values.apiKey })
+ const requestyModels = await getRequestyModels()
- 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 })
- }
+ 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
diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index 8f64a9ba05..e87edffed1 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -27,10 +27,11 @@ export interface ExtensionMessage {
| "workspaceUpdated"
| "invoke"
| "partialMessage"
- | "glamaModels"
| "openRouterModels"
- | "openAiModels"
+ | "glamaModels"
+ | "unboundModels"
| "requestyModels"
+ | "openAiModels"
| "mcpServers"
| "enhancedPrompt"
| "commitSearchResults"
@@ -43,8 +44,6 @@ export interface ExtensionMessage {
| "autoApprovalEnabled"
| "updateCustomMode"
| "deleteCustomMode"
- | "unboundModels"
- | "refreshUnboundModels"
| "currentCheckpointUpdated"
text?: string
action?:
@@ -67,11 +66,11 @@ export interface ExtensionMessage {
path?: string
}>
partialMessage?: ClineMessage
- glamaModels?: Record
- requestyModels?: Record
openRouterModels?: Record
- openAiModels?: string[]
+ glamaModels?: Record
unboundModels?: Record
+ requestyModels?: Record
+ openAiModels?: string[]
mcpServers?: McpServer[]
commits?: GitCommit[]
listApiConfig?: ApiConfigMeta[]
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index 106e6d243b..fde7442cc1 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -40,11 +40,11 @@ export interface WebviewMessage {
| "openFile"
| "openMention"
| "cancelTask"
- | "refreshGlamaModels"
| "refreshOpenRouterModels"
- | "refreshOpenAiModels"
+ | "refreshGlamaModels"
| "refreshUnboundModels"
| "refreshRequestyModels"
+ | "refreshOpenAiModels"
| "alwaysAllowBrowser"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
@@ -71,7 +71,6 @@ export interface WebviewMessage {
| "mcpEnabled"
| "enableMcpServerCreation"
| "searchCommits"
- | "refreshGlamaModels"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "rateLimitSeconds"
diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json
index 1d64f934dc..22564d01a6 100644
--- a/webview-ui/package-lock.json
+++ b/webview-ui/package-lock.json
@@ -3674,6 +3674,7 @@
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz",
"integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==",
+ "license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
@@ -4719,6 +4720,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
+ "license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
diff --git a/webview-ui/src/components/settings/ApiErrorMessage.tsx b/webview-ui/src/components/settings/ApiErrorMessage.tsx
index 4b419957b6..06764a1bfa 100644
--- a/webview-ui/src/components/settings/ApiErrorMessage.tsx
+++ b/webview-ui/src/components/settings/ApiErrorMessage.tsx
@@ -4,13 +4,13 @@ interface ApiErrorMessageProps {
errorMessage: string | undefined
children?: React.ReactNode
}
-const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => {
- return (
-
-
- {errorMessage}
- {children}
+
+export const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => (
+
+
- )
-}
-export default ApiErrorMessage
+ {children}
+
+)
diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx
index 107f2a483a..c30035cef0 100644
--- a/webview-ui/src/components/settings/ApiOptions.tsx
+++ b/webview-ui/src/components/settings/ApiOptions.tsx
@@ -4,8 +4,6 @@ import { Checkbox, Dropdown, Pane, type DropdownOption } from "vscrui"
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import * as vscodemodels from "vscode"
-import { Slider } from "@/components/ui"
-
import {
ApiConfiguration,
ModelInfo,
@@ -33,7 +31,6 @@ import {
unboundDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
- THINKING_BUDGET,
} from "../../../../src/shared/api"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
@@ -44,7 +41,18 @@ import { DROPDOWN_Z_INDEX } from "./styles"
import { ModelPicker } from "./ModelPicker"
import { TemperatureControl } from "./TemperatureControl"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
-import ApiErrorMessage from "./ApiErrorMessage"
+import { ApiErrorMessage } from "./ApiErrorMessage"
+import { ThinkingBudget } from "./ThinkingBudget"
+
+const modelsByProvider: Record
> = {
+ anthropic: anthropicModels,
+ bedrock: bedrockModels,
+ vertex: vertexModels,
+ gemini: geminiModels,
+ "openai-native": openAiNativeModels,
+ deepseek: deepSeekModels,
+ mistral: mistralModels,
+}
interface ApiOptionsProps {
uriScheme: string | undefined
@@ -66,18 +74,23 @@ const ApiOptions = ({
const [ollamaModels, setOllamaModels] = useState([])
const [lmStudioModels, setLmStudioModels] = useState([])
const [vsCodeLmModels, setVsCodeLmModels] = useState([])
+
const [openRouterModels, setOpenRouterModels] = useState>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
+
const [glamaModels, setGlamaModels] = useState>({
[glamaDefaultModelId]: glamaDefaultModelInfo,
})
+
const [unboundModels, setUnboundModels] = useState>({
[unboundDefaultModelId]: unboundDefaultModelInfo,
})
+
const [requestyModels, setRequestyModels] = useState>({
[requestyDefaultModelId]: requestyDefaultModelInfo,
})
+
const [openAiModels, setOpenAiModels] = useState | null>(null)
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
@@ -85,8 +98,6 @@ const ApiOptions = ({
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
- const anthropicThinkingBudget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
-
const noTransform = (value: T) => value
const inputEventTransform = (event: E) => (event as { target: HTMLInputElement })?.target?.value as any
const dropdownEventTransform = (event: DropdownOption | string | undefined) =>
@@ -103,62 +114,87 @@ const ApiOptions = ({
[setApiConfigurationField],
)
- const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
- return normalizeApiConfiguration(apiConfiguration)
- }, [apiConfiguration])
+ const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(
+ () => normalizeApiConfiguration(apiConfiguration),
+ [apiConfiguration],
+ )
- // Pull ollama/lmstudio models
- // Debounced model updates, only executed 250ms after the user stops typing
+ // Debounced refresh model updates, only executed 250ms after the user
+ // stops typing.
useDebounce(
() => {
- if (selectedProvider === "ollama") {
+ if (selectedProvider === "openrouter") {
+ vscode.postMessage({ type: "refreshOpenRouterModels" })
+ } else if (selectedProvider === "glama") {
+ vscode.postMessage({ type: "refreshGlamaModels" })
+ } else if (selectedProvider === "unbound") {
+ vscode.postMessage({ type: "refreshUnboundModels" })
+ } else if (selectedProvider === "requesty") {
+ vscode.postMessage({
+ type: "refreshRequestyModels",
+ values: { apiKey: apiConfiguration?.requestyApiKey },
+ })
+ } else if (selectedProvider === "openai") {
+ vscode.postMessage({
+ type: "refreshOpenAiModels",
+ values: { baseUrl: apiConfiguration?.openAiBaseUrl, apiKey: apiConfiguration?.openAiApiKey },
+ })
+ } else if (selectedProvider === "ollama") {
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
} else if (selectedProvider === "lmstudio") {
vscode.postMessage({ type: "requestLmStudioModels", text: apiConfiguration?.lmStudioBaseUrl })
} else if (selectedProvider === "vscode-lm") {
vscode.postMessage({ type: "requestVsCodeLmModels" })
- } else if (selectedProvider === "openai") {
- vscode.postMessage({
- type: "refreshOpenAiModels",
- values: {
- baseUrl: apiConfiguration?.openAiBaseUrl,
- apiKey: apiConfiguration?.openAiApiKey,
- },
- })
- } else if (selectedProvider === "openrouter") {
- vscode.postMessage({ type: "refreshOpenRouterModels", values: {} })
- } else if (selectedProvider === "glama") {
- vscode.postMessage({ type: "refreshGlamaModels", values: {} })
- } else if (selectedProvider === "requesty") {
- vscode.postMessage({
- type: "refreshRequestyModels",
- values: {
- apiKey: apiConfiguration?.requestyApiKey,
- },
- })
}
},
250,
[
selectedProvider,
- apiConfiguration?.ollamaBaseUrl,
- apiConfiguration?.lmStudioBaseUrl,
+ apiConfiguration?.requestyApiKey,
apiConfiguration?.openAiBaseUrl,
apiConfiguration?.openAiApiKey,
- apiConfiguration?.requestyApiKey,
+ apiConfiguration?.ollamaBaseUrl,
+ apiConfiguration?.lmStudioBaseUrl,
],
)
useEffect(() => {
const apiValidationResult =
validateApiConfiguration(apiConfiguration) ||
- validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels)
- setErrorMessage(apiValidationResult)
- }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels])
+ validateModelId(apiConfiguration, glamaModels, openRouterModels, unboundModels, requestyModels)
- const handleMessage = useCallback((event: MessageEvent) => {
+ setErrorMessage(apiValidationResult)
+ }, [apiConfiguration, glamaModels, openRouterModels, setErrorMessage, unboundModels, requestyModels])
+
+ const onMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
+
switch (message.type) {
+ case "openRouterModels": {
+ const updatedModels = message.openRouterModels ?? {}
+ setOpenRouterModels({ [openRouterDefaultModelId]: openRouterDefaultModelInfo, ...updatedModels })
+ break
+ }
+ case "glamaModels": {
+ const updatedModels = message.glamaModels ?? {}
+ setGlamaModels({ [glamaDefaultModelId]: glamaDefaultModelInfo, ...updatedModels })
+ break
+ }
+ case "unboundModels": {
+ const updatedModels = message.unboundModels ?? {}
+ setUnboundModels({ [unboundDefaultModelId]: unboundDefaultModelInfo, ...updatedModels })
+ break
+ }
+ case "requestyModels": {
+ const updatedModels = message.requestyModels ?? {}
+ setRequestyModels({ [requestyDefaultModelId]: requestyDefaultModelInfo, ...updatedModels })
+ break
+ }
+ case "openAiModels": {
+ const updatedModels = message.openAiModels ?? []
+ setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
+ break
+ }
case "ollamaModels":
{
const newModels = message.ollamaModels ?? []
@@ -177,72 +213,30 @@ const ApiOptions = ({
setVsCodeLmModels(newModels)
}
break
- case "glamaModels": {
- const updatedModels = message.glamaModels ?? {}
- setGlamaModels({
- [glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
- case "openRouterModels": {
- const updatedModels = message.openRouterModels ?? {}
- setOpenRouterModels({
- [openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
- case "openAiModels": {
- const updatedModels = message.openAiModels ?? []
- setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
- break
- }
- case "unboundModels": {
- const updatedModels = message.unboundModels ?? {}
- setUnboundModels(updatedModels)
- break
- }
- case "requestyModels": {
- const updatedModels = message.requestyModels ?? {}
- setRequestyModels({
- [requestyDefaultModelId]: requestyDefaultModelInfo, // in case the extension sent a model list without the default model
- ...updatedModels,
- })
- break
- }
}
}, [])
- useEvent("message", handleMessage)
+ useEvent("message", onMessage)
- const createDropdown = (models: Record) => {
- const options: DropdownOption[] = [
- { value: "", label: "Select a model..." },
- ...Object.keys(models).map((modelId) => ({
- value: modelId,
- label: modelId,
- })),
- ]
-
- return (
- {
- setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value)
- }}
- style={{ width: "100%" }}
- options={options}
- />
- )
- }
+ const selectedProviderModelOptions: DropdownOption[] = useMemo(
+ () =>
+ modelsByProvider[selectedProvider]
+ ? [
+ { value: "", label: "Select a model..." },
+ ...Object.keys(modelsByProvider[selectedProvider]).map((modelId) => ({
+ value: modelId,
+ label: modelId,
+ })),
+ ]
+ : [],
+ [selectedProvider],
+ )
return (
-
+ {errorMessage &&
}
+
{selectedProvider === "anthropic" && (
- Anthropic API Key
+ Anthropic API Key
- Glama API Key
+ Glama API Key
{!apiConfiguration?.glamaApiKey && (
- Requesty API Key
+ Requesty API Key
- OpenAI API Key
+ OpenAI API Key
- Mistral API Key
+ Mistral API Key
- Codestral Base URL (Optional)
+ Codestral Base URL (Optional)
- OpenRouter API Key
+ OpenRouter API Key
{!apiConfiguration?.openRouterApiKey && (
@@ -530,7 +526,7 @@ const ApiOptions = ({
style={{ width: "100%" }}
onInput={handleInputChange("awsProfile")}
placeholder="Enter profile name">
- AWS Profile Name
+ AWS Profile Name
) : (
<>
@@ -541,7 +537,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("awsAccessKey")}
placeholder="Enter Access Key...">
- AWS Access Key
+ AWS Access Key
- AWS Secret Key
+ AWS Secret Key
- AWS Session Token
+ AWS Session Token
>
)}
- AWS Region
+ AWS Region
- Google Cloud Project ID
+ Google Cloud Project ID
- Google Cloud Region
+ Google Cloud Region
- {errorMessage && }
- Gemini API Key
+ Gemini API Key
- Base URL
+ Base URL
- API Key
+ API Key
)}
-
-
+
- Max Output Tokens
+ Max Output Tokens
-
Context Window Size
+
Context Window Size
-
Image Support
+
Image Support
- Computer Use
+ Computer Use
-
Input Price
+
Input Price
-
Output Price
+
Output Price
- Base URL (optional)
+ Base URL (optional)
- Model ID
+ Model ID
- {errorMessage && }
-
{lmStudioModels.length > 0 && (
{" "}
feature to use it with this extension.{" "}
- (Note: Roo Code uses complex prompts and works best
+ (Note: Roo Code uses complex prompts and works best
with Claude models. Less capable models may not work as expected.)
@@ -1154,7 +1141,7 @@ const ApiOptions = ({
type="password"
onInput={handleInputChange("deepSeekApiKey")}
placeholder="Enter API Key...">
- DeepSeek API Key
+ DeepSeek API Key
- Language Model
+ Language Model
{vsCodeLmModels.length > 0 ? (
- Base URL (optional)
+ Base URL (optional)
- Model ID
+ Model ID
{errorMessage && (
@@ -1284,7 +1271,7 @@ const ApiOptions = ({
quickstart guide.
- (Note: Roo Code uses complex prompts and works best
+ (Note: Roo Code uses complex prompts and works best
with Claude models. Less capable models may not work as expected.)
@@ -1299,7 +1286,7 @@ const ApiOptions = ({
type="password"
onChange={handleInputChange("unboundApiKey")}
placeholder="Enter API Key...">
- Unbound API Key
+ Unbound API Key
{!apiConfiguration?.unboundApiKey && (
This key is stored locally and only used to make API requests from this extension.
-
)}
- {selectedProvider === "glama" && (
-
- )}
-
{selectedProvider === "openrouter" && (
)}
+
+ {selectedProvider === "glama" && (
+
+ )}
+
+ {selectedProvider === "unbound" && (
+
+ )}
+
{selectedProvider === "requesty" && (
)}
- {selectedProvider !== "glama" &&
- selectedProvider !== "openrouter" &&
- selectedProvider !== "requesty" &&
- selectedProvider !== "openai" &&
- selectedProvider !== "ollama" &&
- selectedProvider !== "lmstudio" &&
- selectedProvider !== "unbound" && (
- <>
-
-
- Model
-
- {selectedProvider === "anthropic" && createDropdown(anthropicModels)}
- {selectedProvider === "bedrock" && createDropdown(bedrockModels)}
- {selectedProvider === "vertex" && createDropdown(vertexModels)}
- {selectedProvider === "gemini" && createDropdown(geminiModels)}
- {selectedProvider === "openai-native" && createDropdown(openAiNativeModels)}
- {selectedProvider === "deepseek" && createDropdown(deepSeekModels)}
- {selectedProvider === "mistral" && createDropdown(mistralModels)}
-
- {errorMessage && }
- 0 && (
+ <>
+
+
+ Model
+
+
{
+ setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value)
+ }}
+ options={selectedProviderModelOptions}
+ className="w-full"
/>
- >
- )}
-
- {selectedModelInfo && selectedModelInfo.thinking && (
-
-
Thinking Budget
-
-
setApiConfigurationField("anthropicThinking", value[0])}
- />
- {anthropicThinkingBudget}
-
- Number of tokens Claude is allowed to use for its internal reasoning process.
-
-
+
+
+ >
)}
{!fromWelcomeView && (
@@ -1459,6 +1423,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
const getProviderData = (models: Record, defaultId: string) => {
let selectedModelId: string
let selectedModelInfo: ModelInfo
+
if (modelId && modelId in models) {
selectedModelId = modelId
selectedModelInfo = models[modelId]
@@ -1466,8 +1431,10 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
selectedModelId = defaultId
selectedModelInfo = models[defaultId]
}
+
return { selectedProvider: provider, selectedModelId, selectedModelInfo }
}
+
switch (provider) {
case "anthropic":
return getProviderData(anthropicModels, anthropicDefaultModelId)
@@ -1481,12 +1448,6 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
return getProviderData(deepSeekModels, deepSeekDefaultModelId)
case "openai-native":
return getProviderData(openAiNativeModels, openAiNativeDefaultModelId)
- case "glama":
- return {
- selectedProvider: provider,
- selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
- selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo,
- }
case "mistral":
return getProviderData(mistralModels, mistralDefaultModelId)
case "openrouter":
@@ -1495,6 +1456,24 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
selectedModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
selectedModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
}
+ case "glama":
+ return {
+ selectedProvider: provider,
+ selectedModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
+ selectedModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo,
+ }
+ case "unbound":
+ return {
+ selectedProvider: provider,
+ selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId,
+ selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo,
+ }
+ case "requesty":
+ return {
+ selectedProvider: provider,
+ selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
+ selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
+ }
case "openai":
return {
selectedProvider: provider,
@@ -1521,21 +1500,9 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
: "",
selectedModelInfo: {
...openAiModelInfoSaneDefaults,
- supportsImages: false, // VSCode LM API currently doesn't support images
+ supportsImages: false, // VSCode LM API currently doesn't support images.
},
}
- case "unbound":
- return {
- selectedProvider: provider,
- selectedModelId: apiConfiguration?.unboundModelId || unboundDefaultModelId,
- selectedModelInfo: apiConfiguration?.unboundModelInfo || unboundDefaultModelInfo,
- }
- case "requesty":
- return {
- selectedProvider: provider,
- selectedModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
- selectedModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
- }
default:
return getProviderData(anthropicModels, anthropicDefaultModelId)
}
diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx
index fd62bfb97b..5a7737edd5 100644
--- a/webview-ui/src/components/settings/ModelPicker.tsx
+++ b/webview-ui/src/components/settings/ModelPicker.tsx
@@ -1,11 +1,13 @@
+import { useMemo, useState, useCallback, useEffect, useRef } from "react"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
-import { useMemo, useState, useCallback, useEffect } from "react"
+
+import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "@/components/ui/combobox"
+
+import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
import { normalizeApiConfiguration } from "./ApiOptions"
+import { ThinkingBudget } from "./ThinkingBudget"
import { ModelInfoView } from "./ModelInfoView"
-import { ApiConfiguration, ModelInfo } from "../../../../src/shared/api"
-import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem } from "../ui/combobox"
-import ApiErrorMessage from "./ApiErrorMessage"
type ExtractType = NonNullable<
{ [K in keyof ApiConfiguration]: Required[K] extends T ? K : never }[keyof ApiConfiguration]
@@ -14,24 +16,17 @@ type ExtractType = NonNullable<
type ModelIdKeys = NonNullable<
{ [K in keyof ApiConfiguration]: K extends `${string}ModelId` ? K : never }[keyof ApiConfiguration]
>
-declare module "react" {
- interface CSSProperties {
- // Allow CSS variables
- [key: `--${string}`]: string | number
- }
-}
+
interface ModelPickerProps {
- defaultModelId?: string
+ defaultModelId: string
+ defaultModelInfo?: ModelInfo
models: Record | null
modelIdKey: ModelIdKeys
modelInfoKey: ExtractType
serviceName: string
serviceUrl: string
- recommendedModel: string
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void
- defaultModelInfo?: ModelInfo
- errorMessage?: string
}
export const ModelPicker = ({
@@ -41,13 +36,12 @@ export const ModelPicker = ({
modelInfoKey,
serviceName,
serviceUrl,
- recommendedModel,
apiConfiguration,
setApiConfigurationField,
defaultModelInfo,
- errorMessage,
}: ModelPickerProps) => {
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
+ const isInitialized = useRef(false)
const modelIds = useMemo(() => Object.keys(models ?? {}).sort((a, b) => a.localeCompare(b)), [models])
@@ -55,6 +49,7 @@ export const ModelPicker = ({
() => normalizeApiConfiguration(apiConfiguration),
[apiConfiguration],
)
+
const onSelect = useCallback(
(modelId: string) => {
const modelInfo = models?.[modelId]
@@ -63,26 +58,23 @@ export const ModelPicker = ({
},
[modelIdKey, modelInfoKey, models, setApiConfigurationField, defaultModelInfo],
)
+
+ const inputValue = apiConfiguration[modelIdKey]
+
useEffect(() => {
- if (apiConfiguration[modelIdKey] == null && defaultModelId) {
- onSelect(defaultModelId)
+ if (!inputValue && !isInitialized.current) {
+ const initialValue = modelIds.includes(selectedModelId) ? selectedModelId : defaultModelId
+ setApiConfigurationField(modelIdKey, initialValue)
}
- }, [apiConfiguration, defaultModelId, modelIdKey, onSelect])
+
+ isInitialized.current = true
+ }, [inputValue, modelIds, setApiConfigurationField, modelIdKey, selectedModelId, defaultModelId])
return (
<>
Model
-
-
+
+
No model found.
{modelIds.map((model) => (
@@ -92,31 +84,18 @@ export const ModelPicker = ({
))}
-
- {errorMessage ? (
-
-
-
- Note: Roo Code uses complex prompts and works best
- with Claude models. Less capable models may not work as expected.
-
-
-
- ) : (
- selectedModelId &&
- selectedModelInfo && (
-
- )
+
+ {selectedModelId && selectedModelInfo && selectedModelId === inputValue && (
+
)}
The extension automatically fetches the latest list of models available on{" "}
@@ -124,7 +103,7 @@ export const ModelPicker = ({
{serviceName}.
If you're unsure which model to choose, Roo Code works best with{" "}
- onSelect(recommendedModel)}>{recommendedModel}.
+ onSelect(defaultModelId)}>{defaultModelId}.
You can also try searching "free" for no-cost options currently available.
>
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index ee032c3ee0..d3e65a99ea 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -66,21 +66,20 @@ const SettingsView = forwardRef(({ onDone },
terminalOutputLineLimit,
writeDelayMs,
} = cachedState
-
+
//Make sure apiConfiguration is initialized and managed by SettingsView
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
useEffect(() => {
- // Update only when currentApiConfigName is changed
- // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration
+ // Update only when currentApiConfigName is changed.
+ // Expected to be triggered by loadApiConfiguration/upsertApiConfiguration.
if (prevApiConfigName.current === currentApiConfigName) {
return
}
- setCachedState((prevCachedState) => ({
- ...prevCachedState,
- ...extensionState,
- }))
+
+ setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
prevApiConfigName.current = currentApiConfigName
+ // console.log("useEffect: currentApiConfigName changed, setChangeDetected -> false")
setChangeDetected(false)
}, [currentApiConfigName, extensionState, isChangeDetected])
@@ -90,11 +89,10 @@ const SettingsView = forwardRef(({ onDone },
if (prevState[field] === value) {
return prevState
}
+
+ // console.log(`setCachedStateField(${field} -> ${value}): setChangeDetected -> true`)
setChangeDetected(true)
- return {
- ...prevState,
- [field]: value,
- }
+ return { ...prevState, [field]: value }
})
},
[],
@@ -107,15 +105,10 @@ const SettingsView = forwardRef(({ onDone },
return prevState
}
+ // console.log(`setApiConfigurationField(${field} -> ${value}): setChangeDetected -> true`)
setChangeDetected(true)
- return {
- ...prevState,
- apiConfiguration: {
- ...prevState.apiConfiguration,
- [field]: value,
- },
- }
+ return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } }
})
},
[],
@@ -126,14 +119,19 @@ const SettingsView = forwardRef(({ onDone },
if (prevState.experiments?.[id] === enabled) {
return prevState
}
+
+ // console.log("setExperimentEnabled: setChangeDetected -> true")
setChangeDetected(true)
+
return {
...prevState,
experiments: { ...prevState.experiments, [id]: enabled },
}
})
}, [])
+
const isSettingValid = !errorMessage
+
const handleSubmit = () => {
if (isSettingValid) {
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
@@ -160,6 +158,7 @@ const SettingsView = forwardRef(({ onDone },
vscode.postMessage({ type: "updateExperimental", values: experiments })
vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
+ // console.log("handleSubmit: setChangeDetected -> false")
setChangeDetected(false)
}
}
@@ -176,13 +175,7 @@ const SettingsView = forwardRef(({ onDone },
[isChangeDetected],
)
- useImperativeHandle(
- ref,
- () => ({
- checkUnsaveChanges,
- }),
- [checkUnsaveChanges],
- )
+ useImperativeHandle(ref, () => ({ checkUnsaveChanges }), [checkUnsaveChanges])
const onConfirmDialogResult = useCallback((confirm: boolean) => {
if (confirm) {
@@ -200,10 +193,7 @@ const SettingsView = forwardRef(({ onDone },
const newCommands = [...currentCommands, commandInput]
setCachedStateField("allowedCommands", newCommands)
setCommandInput("")
- vscode.postMessage({
- type: "allowedCommands",
- commands: newCommands,
- })
+ vscode.postMessage({ type: "allowedCommands", commands: newCommands })
}
}
diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx
new file mode 100644
index 0000000000..efaa90dc39
--- /dev/null
+++ b/webview-ui/src/components/settings/ThinkingBudget.tsx
@@ -0,0 +1,29 @@
+import { Slider } from "@/components/ui"
+
+import { ApiConfiguration, ModelInfo, THINKING_BUDGET } from "../../../../src/shared/api"
+
+interface ThinkingBudgetProps {
+ apiConfiguration: ApiConfiguration
+ setApiConfigurationField: (field: K, value: ApiConfiguration[K]) => void
+ modelInfo?: ModelInfo
+}
+
+export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => {
+ const budget = apiConfiguration?.anthropicThinking ?? THINKING_BUDGET.default
+
+ return modelInfo && modelInfo.thinking ? (
+
+
Thinking Budget
+
+
setApiConfigurationField("anthropicThinking", value[0])}
+ />
+ {budget}
+
+
+ ) : null
+}
diff --git a/webview-ui/src/components/ui/alert-dialog.tsx b/webview-ui/src/components/ui/alert-dialog.tsx
index 7530cae54d..82a25bf8f7 100644
--- a/webview-ui/src/components/ui/alert-dialog.tsx
+++ b/webview-ui/src/components/ui/alert-dialog.tsx
@@ -4,94 +4,97 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
-const AlertDialog = AlertDialogPrimitive.Root
+function AlertDialog({ ...props }: React.ComponentProps) {
+ return
+}
-const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+function AlertDialogTrigger({ ...props }: React.ComponentProps) {
+ return
+}
-const AlertDialogPortal = AlertDialogPrimitive.Portal
+function AlertDialogPortal({ ...props }: React.ComponentProps) {
+ return
+}
-const AlertDialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
-
-const AlertDialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
- ) {
+ return (
+
-
-))
-AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+ )
+}
-const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
-
-)
-AlertDialogHeader.displayName = "AlertDialogHeader"
+function AlertDialogContent({ className, ...props }: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
-const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
-
-)
-AlertDialogFooter.displayName = "AlertDialogFooter"
+function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const AlertDialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const AlertDialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
+function AlertDialogTitle({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
-const AlertDialogAction = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
-const AlertDialogCancel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+function AlertDialogAction({ className, ...props }: React.ComponentProps) {
+ return
+}
+
+function AlertDialogCancel({ className, ...props }: React.ComponentProps) {
+ return
+}
export {
AlertDialog,
diff --git a/webview-ui/src/components/ui/dialog.tsx b/webview-ui/src/components/ui/dialog.tsx
index 11d5e2d3b0..ed3160f692 100644
--- a/webview-ui/src/components/ui/dialog.tsx
+++ b/webview-ui/src/components/ui/dialog.tsx
@@ -1,96 +1,108 @@
-"use client"
-
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
-import { Cross2Icon } from "@radix-ui/react-icons"
+import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
-const Dialog = DialogPrimitive.Root
+function Dialog({ ...props }: React.ComponentProps) {
+ return
+}
-const DialogTrigger = DialogPrimitive.Trigger
+function DialogTrigger({ ...props }: React.ComponentProps) {
+ return
+}
-const DialogPortal = DialogPrimitive.Portal
+function DialogPortal({ ...props }: React.ComponentProps) {
+ return
+}
-const DialogClose = DialogPrimitive.Close
+function DialogClose({ ...props }: React.ComponentProps) {
+ return
+}
-const DialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
-
-const DialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
-
- ) {
+ return (
+
- {children}
-
-
- Close
-
-
-
-))
-DialogContent.displayName = DialogPrimitive.Content.displayName
+ {...props}
+ />
+ )
+}
-const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
-
-)
-DialogHeader.displayName = "DialogHeader"
+function DialogContent({ className, children, ...props }: React.ComponentProps) {
+ return (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+ )
+}
-const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
-
-)
-DialogFooter.displayName = "DialogFooter"
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const DialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DialogTitle.displayName = DialogPrimitive.Title.displayName
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
-const DialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DialogDescription.displayName = DialogPrimitive.Description.displayName
+function DialogTitle({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogDescription({ className, ...props }: React.ComponentProps) {
+ return (
+
+ )
+}
export {
Dialog,
- DialogPortal,
- DialogOverlay,
- DialogTrigger,
DialogClose,
DialogContent,
- DialogHeader,
- DialogFooter,
- DialogTitle,
DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
}
diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx
index 5d880efc0b..ae674c895f 100644
--- a/webview-ui/src/components/welcome/WelcomeView.tsx
+++ b/webview-ui/src/components/welcome/WelcomeView.tsx
@@ -12,16 +12,14 @@ const WelcomeView = () => {
const handleSubmit = useCallback(() => {
const error = validateApiConfiguration(apiConfiguration)
+
if (error) {
setErrorMessage(error)
return
}
+
setErrorMessage(undefined)
- vscode.postMessage({
- type: "upsertApiConfiguration",
- text: currentApiConfigName,
- apiConfiguration,
- })
+ vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
}, [apiConfiguration, currentApiConfigName])
return (
diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts
index 97c702637c..82af23ab49 100644
--- a/webview-ui/src/utils/validate.ts
+++ b/webview-ui/src/utils/validate.ts
@@ -1,74 +1,83 @@
-import { ApiConfiguration } from "../../../src/shared/api"
-import { ModelInfo } from "../../../src/shared/api"
+import { ApiConfiguration, ModelInfo } from "../../../src/shared/api"
+
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
- if (apiConfiguration) {
- switch (apiConfiguration.apiProvider) {
- case "anthropic":
- if (!apiConfiguration.apiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "glama":
- if (!apiConfiguration.glamaApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "bedrock":
- if (!apiConfiguration.awsRegion) {
- return "You must choose a region to use with AWS Bedrock."
- }
- break
- case "openrouter":
- if (!apiConfiguration.openRouterApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "vertex":
- if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) {
- return "You must provide a valid Google Cloud Project ID and Region."
- }
- break
- case "gemini":
- if (!apiConfiguration.geminiApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "openai-native":
- if (!apiConfiguration.openAiNativeApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "mistral":
- if (!apiConfiguration.mistralApiKey) {
- return "You must provide a valid API key or choose a different provider."
- }
- break
- case "openai":
- if (
- !apiConfiguration.openAiBaseUrl ||
- !apiConfiguration.openAiApiKey ||
- !apiConfiguration.openAiModelId
- ) {
- return "You must provide a valid base URL, API key, and model ID."
- }
- break
- case "ollama":
- if (!apiConfiguration.ollamaModelId) {
- return "You must provide a valid model ID."
- }
- break
- case "lmstudio":
- if (!apiConfiguration.lmStudioModelId) {
- return "You must provide a valid model ID."
- }
- break
- case "vscode-lm":
- if (!apiConfiguration.vsCodeLmModelSelector) {
- return "You must provide a valid model selector."
- }
- break
- }
+ if (!apiConfiguration) {
+ return undefined
}
+
+ switch (apiConfiguration.apiProvider) {
+ case "openrouter":
+ if (!apiConfiguration.openRouterApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "glama":
+ if (!apiConfiguration.glamaApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "unbound":
+ if (!apiConfiguration.unboundApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "requesty":
+ if (!apiConfiguration.requestyApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "anthropic":
+ if (!apiConfiguration.apiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "bedrock":
+ if (!apiConfiguration.awsRegion) {
+ return "You must choose a region to use with AWS Bedrock."
+ }
+ break
+ case "vertex":
+ if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) {
+ return "You must provide a valid Google Cloud Project ID and Region."
+ }
+ break
+ case "gemini":
+ if (!apiConfiguration.geminiApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "openai-native":
+ if (!apiConfiguration.openAiNativeApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "mistral":
+ if (!apiConfiguration.mistralApiKey) {
+ return "You must provide a valid API key."
+ }
+ break
+ case "openai":
+ if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) {
+ return "You must provide a valid base URL, API key, and model ID."
+ }
+ break
+ case "ollama":
+ if (!apiConfiguration.ollamaModelId) {
+ return "You must provide a valid model ID."
+ }
+ break
+ case "lmstudio":
+ if (!apiConfiguration.lmStudioModelId) {
+ return "You must provide a valid model ID."
+ }
+ break
+ case "vscode-lm":
+ if (!apiConfiguration.vsCodeLmModelSelector) {
+ return "You must provide a valid model selector."
+ }
+ break
+ }
+
return undefined
}
@@ -77,40 +86,81 @@ export function validateModelId(
glamaModels?: Record,
openRouterModels?: Record,
unboundModels?: Record,
+ requestyModels?: Record,
): string | undefined {
- if (apiConfiguration) {
- switch (apiConfiguration.apiProvider) {
- case "glama":
- const glamaModelId = apiConfiguration.glamaModelId
- if (!glamaModelId) {
- return "You must provide a model ID."
- }
- if (glamaModels && !Object.keys(glamaModels).includes(glamaModelId)) {
- // even if the model list endpoint failed, extensionstatecontext will always have the default model info
- return "The model ID you provided is not available. Please choose a different model."
- }
- break
- case "openrouter":
- const modelId = apiConfiguration.openRouterModelId
- if (!modelId) {
- return "You must provide a model ID."
- }
- if (openRouterModels && !Object.keys(openRouterModels).includes(modelId)) {
- // even if the model list endpoint failed, extensionstatecontext will always have the default model info
- return "The model ID you provided is not available. Please choose a different model."
- }
- break
- case "unbound":
- const unboundModelId = apiConfiguration.unboundModelId
- if (!unboundModelId) {
- return "You must provide a model ID."
- }
- if (unboundModels && !Object.keys(unboundModels).includes(unboundModelId)) {
- // even if the model list endpoint failed, extensionstatecontext will always have the default model info
- return "The model ID you provided is not available. Please choose a different model."
- }
- break
- }
+ if (!apiConfiguration) {
+ return undefined
}
+
+ switch (apiConfiguration.apiProvider) {
+ case "openrouter":
+ const modelId = apiConfiguration.openRouterModelId
+
+ if (!modelId) {
+ return "You must provide a model ID."
+ }
+
+ if (
+ openRouterModels &&
+ Object.keys(openRouterModels).length > 1 &&
+ !Object.keys(openRouterModels).includes(modelId)
+ ) {
+ return `The model ID (${modelId}) you provided is not available. Please choose a different model.`
+ }
+
+ break
+
+ case "glama":
+ const glamaModelId = apiConfiguration.glamaModelId
+
+ if (!glamaModelId) {
+ return "You must provide a model ID."
+ }
+
+ if (
+ glamaModels &&
+ Object.keys(glamaModels).length > 1 &&
+ !Object.keys(glamaModels).includes(glamaModelId)
+ ) {
+ return `The model ID (${glamaModelId}) you provided is not available. Please choose a different model.`
+ }
+
+ break
+
+ case "unbound":
+ const unboundModelId = apiConfiguration.unboundModelId
+
+ if (!unboundModelId) {
+ return "You must provide a model ID."
+ }
+
+ if (
+ unboundModels &&
+ Object.keys(unboundModels).length > 1 &&
+ !Object.keys(unboundModels).includes(unboundModelId)
+ ) {
+ return `The model ID (${unboundModelId}) you provided is not available. Please choose a different model.`
+ }
+
+ break
+
+ case "requesty":
+ const requestyModelId = apiConfiguration.requestyModelId
+
+ if (!requestyModelId) {
+ return "You must provide a model ID."
+ }
+
+ if (
+ requestyModels &&
+ Object.keys(requestyModels).length > 1 &&
+ !Object.keys(requestyModels).includes(requestyModelId)
+ ) {
+ return `The model ID (${requestyModelId}) you provided is not available. Please choose a different model.`
+ }
+
+ break
+ }
+
return undefined
}
From 44724e5881ab8daefa7b80bb6c52e5f1bb8a828e Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 26 Feb 2025 07:06:25 +0000
Subject: [PATCH 30/32] changeset version bump
---
.changeset/cold-poems-change.md | 5 -----
.changeset/real-ties-destroy.md | 5 -----
.changeset/shaggy-spies-kneel.md | 5 -----
.changeset/swift-kings-attack.md | 5 -----
CHANGELOG.md | 9 +++++++++
package-lock.json | 4 ++--
package.json | 2 +-
7 files changed, 12 insertions(+), 23 deletions(-)
delete mode 100644 .changeset/cold-poems-change.md
delete mode 100644 .changeset/real-ties-destroy.md
delete mode 100644 .changeset/shaggy-spies-kneel.md
delete mode 100644 .changeset/swift-kings-attack.md
diff --git a/.changeset/cold-poems-change.md b/.changeset/cold-poems-change.md
deleted file mode 100644
index 41693ccdfc..0000000000
--- a/.changeset/cold-poems-change.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-v3.7.5
diff --git a/.changeset/real-ties-destroy.md b/.changeset/real-ties-destroy.md
deleted file mode 100644
index a2e9ba8eb0..0000000000
--- a/.changeset/real-ties-destroy.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Fix model picker
diff --git a/.changeset/shaggy-spies-kneel.md b/.changeset/shaggy-spies-kneel.md
deleted file mode 100644
index d137cf85ef..0000000000
--- a/.changeset/shaggy-spies-kneel.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Add drag-and-drop for files
diff --git a/.changeset/swift-kings-attack.md b/.changeset/swift-kings-attack.md
deleted file mode 100644
index 8a8a425611..0000000000
--- a/.changeset/swift-kings-attack.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"roo-cline": patch
----
-
-Pass "thinking" params to OpenRouter
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52fb754097..7a9b4b57bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Roo Code Changelog
+## 3.7.5
+
+### Patch Changes
+
+- v3.7.5
+- Fix model picker
+- Add drag-and-drop for files
+- Pass "thinking" params to OpenRouter
+
## [3.7.4]
- Fix a bug that prevented the "Thinking" setting from properly updating when switching profiles.
diff --git a/package-lock.json b/package-lock.json
index 4bcdf8136d..a6c75bd69b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "3.7.4",
+ "version": "3.7.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "3.7.4",
+ "version": "3.7.5",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
diff --git a/package.json b/package.json
index 28045436e6..40bb6a545d 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Code (prev. Roo Cline)",
"description": "A whole dev team of AI agents in your editor.",
"publisher": "RooVeterinaryInc",
- "version": "3.7.4",
+ "version": "3.7.5",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
From 319f16eb0aabb8033ef94d006b983bdbb45a1eec Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 23:11:42 -0800
Subject: [PATCH 31/32] Update CHANGELOG
---
CHANGELOG.md | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a9b4b57bf..ee107be67a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,11 @@
# Roo Code Changelog
-## 3.7.5
+## [3.7.5]
-### Patch Changes
-
-- v3.7.5
-- Fix model picker
+- Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173))
+- Fix various issues with the model picker
- Add drag-and-drop for files
-- Pass "thinking" params to OpenRouter
+- Enable the "Thinking Budget" slider for Claude 3.7 Sonnet on OpenRouter
## [3.7.4]
From da1b31765ed05cab6c1d0e25cbae6748aa7ce89a Mon Sep 17 00:00:00 2001
From: cte
Date: Tue, 25 Feb 2025 23:13:52 -0800
Subject: [PATCH 32/32] Update CHANGELOG
---
CHANGELOG.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee107be67a..02a4a30cbd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,8 @@
## [3.7.5]
- Fix context window truncation math (see [#1173](https://github.com/RooVetGit/Roo-Code/issues/1173))
-- Fix various issues with the model picker
+- Fix various issues with the model picker (thanks @System233!)
+- Fix model input / output cost parsing (thanks @System233!)
- Add drag-and-drop for files
- Enable the "Thinking Budget" slider for Claude 3.7 Sonnet on OpenRouter