mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Add openrouter model validation
This commit is contained in:
@@ -36,9 +36,10 @@ import OpenRouterModelPicker, { ModelDescriptionMarkdown } from "./OpenRouterMod
|
|||||||
interface ApiOptionsProps {
|
interface ApiOptionsProps {
|
||||||
showModelOptions: boolean
|
showModelOptions: boolean
|
||||||
apiErrorMessage?: string
|
apiErrorMessage?: string
|
||||||
|
modelIdErrorMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ApiOptions = ({ showModelOptions, apiErrorMessage }: ApiOptionsProps) => {
|
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) => {
|
||||||
const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState()
|
const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState()
|
||||||
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
||||||
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
|
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
|
||||||
@@ -544,6 +545,17 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage }: ApiOptionsProps) => {
|
|||||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} />
|
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{modelIdErrorMessage && (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "-10px 0 4px 0",
|
||||||
|
fontSize: 12,
|
||||||
|
color: "var(--vscode-errorForeground)",
|
||||||
|
}}>
|
||||||
|
{modelIdErrorMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||||
import Fuse from "fuse.js"
|
import Fuse from "fuse.js"
|
||||||
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
|
import React, { memo, useEffect, useMemo, useRef, useState, KeyboardEvent } from "react"
|
||||||
import { useRemark } from "react-remark"
|
import { useRemark } from "react-remark"
|
||||||
import { useMount } from "react-use"
|
import { useMount } from "react-use"
|
||||||
import styled from "styled-components"
|
import styled from "styled-components"
|
||||||
@@ -8,12 +8,15 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
|
|||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
import { ModelInfoView, normalizeApiConfiguration } from "./ApiOptions"
|
||||||
import { highlight } from "../history/HistoryView"
|
import { highlight } from "../history/HistoryView"
|
||||||
|
import { openRouterDefaultModelId } from "../../../../src/shared/api"
|
||||||
|
|
||||||
const OpenRouterModelPicker: React.FC = () => {
|
const OpenRouterModelPicker: React.FC = () => {
|
||||||
const { apiConfiguration, setApiConfiguration, openRouterModels } = useExtensionState()
|
const { apiConfiguration, setApiConfiguration, openRouterModels } = useExtensionState()
|
||||||
const [searchTerm, setSearchTerm] = useState("")
|
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openRouterModelId || openRouterDefaultModelId)
|
||||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
|
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||||
|
|
||||||
const handleModelChange = (newModelId: string) => {
|
const handleModelChange = (newModelId: string) => {
|
||||||
setApiConfiguration({
|
setApiConfiguration({
|
||||||
@@ -22,7 +25,6 @@ const OpenRouterModelPicker: React.FC = () => {
|
|||||||
openRouterModelInfo: openRouterModels[newModelId],
|
openRouterModelInfo: openRouterModels[newModelId],
|
||||||
})
|
})
|
||||||
setSearchTerm(newModelId)
|
setSearchTerm(newModelId)
|
||||||
setIsDropdownVisible(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { selectedModelId, selectedModelInfo } = useMemo(() => {
|
const { selectedModelId, selectedModelInfo } = useMemo(() => {
|
||||||
@@ -46,15 +48,17 @@ const OpenRouterModelPicker: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const searchableItems = useMemo(() => {
|
const modelIds = useMemo(() => {
|
||||||
return Object.keys(openRouterModels)
|
return Object.keys(openRouterModels).sort((a, b) => a.localeCompare(b))
|
||||||
.sort((a, b) => a.localeCompare(b))
|
|
||||||
.map((id) => ({
|
|
||||||
id,
|
|
||||||
html: id,
|
|
||||||
}))
|
|
||||||
}, [openRouterModels])
|
}, [openRouterModels])
|
||||||
|
|
||||||
|
const searchableItems = useMemo(() => {
|
||||||
|
return modelIds.map((id) => ({
|
||||||
|
id,
|
||||||
|
html: id,
|
||||||
|
}))
|
||||||
|
}, [modelIds])
|
||||||
|
|
||||||
const fuse = useMemo(() => {
|
const fuse = useMemo(() => {
|
||||||
return new Fuse(searchableItems, {
|
return new Fuse(searchableItems, {
|
||||||
keys: ["html"], // highlight function will update this
|
keys: ["html"], // highlight function will update this
|
||||||
@@ -75,6 +79,49 @@ const OpenRouterModelPicker: React.FC = () => {
|
|||||||
return results
|
return results
|
||||||
}, [searchableItems, searchTerm, fuse])
|
}, [searchableItems, searchTerm, fuse])
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (!isDropdownVisible) return
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowDown":
|
||||||
|
event.preventDefault()
|
||||||
|
setSelectedIndex((prev) => (prev < modelSearchResults.length - 1 ? prev + 1 : prev))
|
||||||
|
break
|
||||||
|
case "ArrowUp":
|
||||||
|
event.preventDefault()
|
||||||
|
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev))
|
||||||
|
break
|
||||||
|
case "Enter":
|
||||||
|
event.preventDefault()
|
||||||
|
if (selectedIndex >= 0 && selectedIndex < modelSearchResults.length) {
|
||||||
|
handleModelChange(modelSearchResults[selectedIndex].id)
|
||||||
|
setIsDropdownVisible(false)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "Escape":
|
||||||
|
setIsDropdownVisible(false)
|
||||||
|
setSelectedIndex(-1)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasInfo = useMemo(() => {
|
||||||
|
return modelIds.some((id) => id.toLowerCase() === searchTerm.toLowerCase())
|
||||||
|
}, [modelIds, searchTerm])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedIndex(-1)
|
||||||
|
}, [searchTerm])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedIndex >= 0 && itemRefs.current[selectedIndex]) {
|
||||||
|
itemRefs.current[selectedIndex]?.scrollIntoView({
|
||||||
|
block: "nearest",
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [selectedIndex])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style>
|
<style>
|
||||||
@@ -94,16 +141,20 @@ const OpenRouterModelPicker: React.FC = () => {
|
|||||||
placeholder="Search and select a model..."
|
placeholder="Search and select a model..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
setSearchTerm((e.target as HTMLInputElement).value)
|
handleModelChange((e.target as HTMLInputElement)?.value?.toLowerCase())
|
||||||
setIsDropdownVisible(true)
|
setIsDropdownVisible(true)
|
||||||
}}
|
}}
|
||||||
onFocus={() => setIsDropdownVisible(true)}
|
onFocus={() => setIsDropdownVisible(true)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
style={{ width: "100%" }}>
|
style={{ width: "100%" }}>
|
||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<div
|
<div
|
||||||
className="input-icon-button codicon codicon-close"
|
className="input-icon-button codicon codicon-close"
|
||||||
aria-label="Clear search"
|
aria-label="Clear search"
|
||||||
onClick={() => setSearchTerm("")}
|
onClick={() => {
|
||||||
|
handleModelChange("")
|
||||||
|
setIsDropdownVisible(true)
|
||||||
|
}}
|
||||||
slot="end"
|
slot="end"
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -116,10 +167,16 @@ const OpenRouterModelPicker: React.FC = () => {
|
|||||||
</VSCodeTextField>
|
</VSCodeTextField>
|
||||||
{isDropdownVisible && (
|
{isDropdownVisible && (
|
||||||
<DropdownList>
|
<DropdownList>
|
||||||
{modelSearchResults.map((item) => (
|
{modelSearchResults.map((item, index) => (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => handleModelChange(item.id)}
|
ref={(el) => (itemRefs.current[index] = el)}
|
||||||
|
isSelected={index === selectedIndex}
|
||||||
|
onMouseEnter={() => setSelectedIndex(index)}
|
||||||
|
onClick={() => {
|
||||||
|
handleModelChange(item.id)
|
||||||
|
setIsDropdownVisible(false)
|
||||||
|
}}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: item.html,
|
__html: item.html,
|
||||||
}}
|
}}
|
||||||
@@ -129,7 +186,22 @@ const OpenRouterModelPicker: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</DropdownWrapper>
|
</DropdownWrapper>
|
||||||
|
|
||||||
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} />
|
{hasInfo ? (
|
||||||
|
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} />
|
||||||
|
) : (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: "12px",
|
||||||
|
marginTop: 0,
|
||||||
|
color: "var(--vscode-descriptionForeground)",
|
||||||
|
}}>
|
||||||
|
You can use{" "}
|
||||||
|
<VSCodeLink style={{ display: "inline", fontSize: "inherit" }} href="https://openrouter.ai/models">
|
||||||
|
any model on OpenRouter
|
||||||
|
</VSCodeLink>{" "}
|
||||||
|
with Cline. (Try searching for "free" to see if there are free models currently available.)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -157,9 +229,14 @@ const DropdownList = styled.div`
|
|||||||
border-bottom-right-radius: 3px;
|
border-bottom-right-radius: 3px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const DropdownItem = styled.div`
|
const DropdownItem = styled.div<{ isSelected: boolean }>`
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: normal;
|
||||||
|
|
||||||
|
background-color: ${({ isSelected }) => (isSelected ? "var(--vscode-list-activeSelectionBackground)" : "inherit")};
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--vscode-list-activeSelectionBackground);
|
background-color: var(--vscode-list-activeSelectionBackground);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
|
||||||
import { memo, useEffect, useState } from "react"
|
import { memo, useEffect, useState } from "react"
|
||||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
import { validateApiConfiguration } from "../../utils/validate"
|
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
|
||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
import ApiOptions from "./ApiOptions"
|
import ApiOptions from "./ApiOptions"
|
||||||
|
|
||||||
@@ -19,15 +19,17 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
setCustomInstructions,
|
setCustomInstructions,
|
||||||
alwaysAllowReadOnly,
|
alwaysAllowReadOnly,
|
||||||
setAlwaysAllowReadOnly,
|
setAlwaysAllowReadOnly,
|
||||||
|
openRouterModels,
|
||||||
} = useExtensionState()
|
} = useExtensionState()
|
||||||
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
|
||||||
|
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
const apiValidationResult = validateApiConfiguration(apiConfiguration)
|
||||||
|
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
|
||||||
|
|
||||||
setApiErrorMessage(apiValidationResult)
|
setApiErrorMessage(apiValidationResult)
|
||||||
|
setModelIdErrorMessage(modelIdValidationResult)
|
||||||
if (!apiValidationResult) {
|
if (!apiValidationResult && !modelIdValidationResult) {
|
||||||
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
|
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
|
||||||
vscode.postMessage({ type: "customInstructions", text: customInstructions })
|
vscode.postMessage({ type: "customInstructions", text: customInstructions })
|
||||||
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
|
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
|
||||||
@@ -37,6 +39,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setApiErrorMessage(undefined)
|
setApiErrorMessage(undefined)
|
||||||
|
setModelIdErrorMessage(undefined)
|
||||||
}, [apiConfiguration])
|
}, [apiConfiguration])
|
||||||
|
|
||||||
// validate as soon as the component is mounted
|
// validate as soon as the component is mounted
|
||||||
@@ -82,7 +85,11 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
|
|||||||
<div
|
<div
|
||||||
style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
|
style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
|
||||||
<div style={{ marginBottom: 5 }}>
|
<div style={{ marginBottom: 5 }}>
|
||||||
<ApiOptions showModelOptions={true} apiErrorMessage={apiErrorMessage} />
|
<ApiOptions
|
||||||
|
showModelOptions={true}
|
||||||
|
apiErrorMessage={apiErrorMessage}
|
||||||
|
modelIdErrorMessage={modelIdErrorMessage}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 5 }}>
|
<div style={{ marginBottom: 5 }}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiConfiguration } from "../../../src/shared/api"
|
import { ApiConfiguration } from "../../../src/shared/api"
|
||||||
|
import { ModelInfo } from "../../../src/shared/api"
|
||||||
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
|
export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): string | undefined {
|
||||||
if (apiConfiguration) {
|
if (apiConfiguration) {
|
||||||
switch (apiConfiguration.apiProvider) {
|
switch (apiConfiguration.apiProvider) {
|
||||||
@@ -14,8 +14,8 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
case "openrouter":
|
case "openrouter":
|
||||||
if (!apiConfiguration.openRouterApiKey || !apiConfiguration.openRouterModelId) {
|
if (!apiConfiguration.openRouterApiKey) {
|
||||||
return "You must provide a valid API key and model ID or choose a different provider."
|
return "You must provide a valid API key or choose a different provider."
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case "vertex":
|
case "vertex":
|
||||||
@@ -51,3 +51,23 @@ export function validateApiConfiguration(apiConfiguration?: ApiConfiguration): s
|
|||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateModelId(
|
||||||
|
apiConfiguration?: ApiConfiguration,
|
||||||
|
openRouterModels?: Record<string, ModelInfo>
|
||||||
|
): string | undefined {
|
||||||
|
if (apiConfiguration) {
|
||||||
|
switch (apiConfiguration.apiProvider) {
|
||||||
|
case "openrouter":
|
||||||
|
const modelId = apiConfiguration.openRouterModelId
|
||||||
|
if (!modelId) {
|
||||||
|
return "You must provide a model ID. If you're not sure which model to choose, Cline works best with Claude 3.5 Sonnet (anthropic/claude-3.5-sonnet)."
|
||||||
|
}
|
||||||
|
if (openRouterModels && !Object.keys(openRouterModels).includes(modelId)) {
|
||||||
|
return "The model ID you provided is not available. Please choose a different model."
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user