mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merge pull request #1202 from RooVetGit/cte/fix-model-picker
Fix model picker
This commit is contained in:
commit
bdc96a0681
15 changed files with 664 additions and 621 deletions
5
.changeset/real-ties-destroy.md
Normal file
5
.changeset/real-ties-destroy.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Fix model picker
|
||||
|
|
@ -42,26 +42,33 @@ export class RequestyHandler extends OpenAiHandler {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getRequestyModels({ apiKey }: { apiKey?: string }) {
|
||||
export async function getRequestyModels() {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
|
||||
if (!apiKey) {
|
||||
return models
|
||||
}
|
||||
|
||||
try {
|
||||
const config: Record<string, any> = {}
|
||||
config["headers"] = { Authorization: `Bearer ${apiKey}` }
|
||||
|
||||
const response = await axios.get("https://router.requesty.ai/v1/models", config)
|
||||
const 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, ModelInfo>
|
||||
requestyModels?: Record<string, ModelInfo>
|
||||
openRouterModels?: Record<string, ModelInfo>
|
||||
openAiModels?: string[]
|
||||
glamaModels?: Record<string, ModelInfo>
|
||||
unboundModels?: Record<string, ModelInfo>
|
||||
requestyModels?: Record<string, ModelInfo>
|
||||
openAiModels?: string[]
|
||||
mcpServers?: McpServer[]
|
||||
commits?: GitCommit[]
|
||||
listApiConfig?: ApiConfigMeta[]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
2
webview-ui/package-lock.json
generated
2
webview-ui/package-lock.json
generated
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ interface ApiErrorMessageProps {
|
|||
errorMessage: string | undefined
|
||||
children?: React.ReactNode
|
||||
}
|
||||
const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => {
|
||||
return (
|
||||
<div className="text-vscode-errorForeground text-sm">
|
||||
<span style={{ fontSize: "2em" }} className={`codicon codicon-close align-middle mr-1`} />
|
||||
{errorMessage}
|
||||
{children}
|
||||
|
||||
export const ApiErrorMessage = ({ errorMessage, children }: ApiErrorMessageProps) => (
|
||||
<div className="flex flex-col gap-2 text-vscode-errorForeground text-sm">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<div className="codicon codicon-close" />
|
||||
<div>{errorMessage}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ApiErrorMessage
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<string, Record<string, ModelInfo>> = {
|
||||
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<string[]>([])
|
||||
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
|
||||
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
|
||||
|
||||
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
||||
})
|
||||
|
||||
const [glamaModels, setGlamaModels] = useState<Record<string, ModelInfo>>({
|
||||
[glamaDefaultModelId]: glamaDefaultModelInfo,
|
||||
})
|
||||
|
||||
const [unboundModels, setUnboundModels] = useState<Record<string, ModelInfo>>({
|
||||
[unboundDefaultModelId]: unboundDefaultModelInfo,
|
||||
})
|
||||
|
||||
const [requestyModels, setRequestyModels] = useState<Record<string, ModelInfo>>({
|
||||
[requestyDefaultModelId]: requestyDefaultModelInfo,
|
||||
})
|
||||
|
||||
const [openAiModels, setOpenAiModels] = useState<Record<string, ModelInfo> | 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 = <T,>(value: T) => value
|
||||
const inputEventTransform = <E,>(event: E) => (event as { target: HTMLInputElement })?.target?.value as any
|
||||
const dropdownEventTransform = <T,>(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<string, ModelInfo>) => {
|
||||
const options: DropdownOption[] = [
|
||||
{ value: "", label: "Select a model..." },
|
||||
...Object.keys(models).map((modelId) => ({
|
||||
value: modelId,
|
||||
label: modelId,
|
||||
})),
|
||||
]
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
id="model-id"
|
||||
value={selectedModelId}
|
||||
onChange={(value) => {
|
||||
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 (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
|
||||
<div className="dropdown-container">
|
||||
<label htmlFor="api-provider">
|
||||
<span style={{ fontWeight: 500 }}>API Provider</span>
|
||||
<label htmlFor="api-provider" className="font-medium">
|
||||
API Provider
|
||||
</label>
|
||||
<Dropdown
|
||||
id="api-provider"
|
||||
|
|
@ -269,6 +263,8 @@ const ApiOptions = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
|
||||
|
||||
{selectedProvider === "anthropic" && (
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
|
|
@ -277,7 +273,7 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onInput={handleInputChange("apiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>Anthropic API Key</span>
|
||||
<span className="font-medium">Anthropic API Key</span>
|
||||
</VSCodeTextField>
|
||||
|
||||
<Checkbox
|
||||
|
|
@ -328,7 +324,7 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onInput={handleInputChange("glamaApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>Glama API Key</span>
|
||||
<span className="font-medium">Glama API Key</span>
|
||||
</VSCodeTextField>
|
||||
{!apiConfiguration?.glamaApiKey && (
|
||||
<VSCodeButtonLink
|
||||
|
|
@ -357,7 +353,7 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onInput={handleInputChange("requestyApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>Requesty API Key</span>
|
||||
<span className="font-medium">Requesty API Key</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
|
|
@ -378,7 +374,7 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onInput={handleInputChange("openAiNativeApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>OpenAI API Key</span>
|
||||
<span className="font-medium">OpenAI API Key</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
|
|
@ -406,7 +402,7 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onInput={handleInputChange("mistralApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>Mistral API Key</span>
|
||||
<span className="font-medium">Mistral API Key</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
|
|
@ -435,7 +431,7 @@ const ApiOptions = ({
|
|||
type="url"
|
||||
onInput={handleInputChange("mistralCodestralUrl")}
|
||||
placeholder="Default: https://codestral.mistral.ai">
|
||||
<span style={{ fontWeight: 500 }}>Codestral Base URL (Optional)</span>
|
||||
<span className="font-medium">Codestral Base URL (Optional)</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
|
|
@ -458,7 +454,7 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onInput={handleInputChange("openRouterApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>OpenRouter API Key</span>
|
||||
<span className="font-medium">OpenRouter API Key</span>
|
||||
</VSCodeTextField>
|
||||
{!apiConfiguration?.openRouterApiKey && (
|
||||
<p>
|
||||
|
|
@ -530,7 +526,7 @@ const ApiOptions = ({
|
|||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("awsProfile")}
|
||||
placeholder="Enter profile name">
|
||||
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
|
||||
<span className="font-medium">AWS Profile Name</span>
|
||||
</VSCodeTextField>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -541,7 +537,7 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onInput={handleInputChange("awsAccessKey")}
|
||||
placeholder="Enter Access Key...">
|
||||
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
|
||||
<span className="font-medium">AWS Access Key</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsSecretKey || ""}
|
||||
|
|
@ -549,7 +545,7 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onInput={handleInputChange("awsSecretKey")}
|
||||
placeholder="Enter Secret Key...">
|
||||
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
|
||||
<span className="font-medium">AWS Secret Key</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsSessionToken || ""}
|
||||
|
|
@ -557,13 +553,13 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onInput={handleInputChange("awsSessionToken")}
|
||||
placeholder="Enter Session Token...">
|
||||
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
|
||||
<span className="font-medium">AWS Session Token</span>
|
||||
</VSCodeTextField>
|
||||
</>
|
||||
)}
|
||||
<div className="dropdown-container">
|
||||
<label htmlFor="aws-region-dropdown">
|
||||
<span style={{ fontWeight: 500 }}>AWS Region</span>
|
||||
<span className="font-medium">AWS Region</span>
|
||||
</label>
|
||||
<Dropdown
|
||||
id="aws-region-dropdown"
|
||||
|
|
@ -615,11 +611,11 @@ const ApiOptions = ({
|
|||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("vertexProjectId")}
|
||||
placeholder="Enter Project ID...">
|
||||
<span style={{ fontWeight: 500 }}>Google Cloud Project ID</span>
|
||||
<span className="font-medium">Google Cloud Project ID</span>
|
||||
</VSCodeTextField>
|
||||
<div className="dropdown-container">
|
||||
<label htmlFor="vertex-region-dropdown">
|
||||
<span style={{ fontWeight: 500 }}>Google Cloud Region</span>
|
||||
<span className="font-medium">Google Cloud Region</span>
|
||||
</label>
|
||||
<Dropdown
|
||||
id="vertex-region-dropdown"
|
||||
|
|
@ -636,7 +632,6 @@ const ApiOptions = ({
|
|||
]}
|
||||
/>
|
||||
</div>
|
||||
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
|
|
@ -668,7 +663,7 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onInput={handleInputChange("geminiApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>Gemini API Key</span>
|
||||
<span className="font-medium">Gemini API Key</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
|
|
@ -696,7 +691,7 @@ const ApiOptions = ({
|
|||
type="url"
|
||||
onInput={handleInputChange("openAiBaseUrl")}
|
||||
placeholder={"Enter base URL..."}>
|
||||
<span style={{ fontWeight: 500 }}>Base URL</span>
|
||||
<span className="font-medium">Base URL</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiApiKey || ""}
|
||||
|
|
@ -704,19 +699,18 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onInput={handleInputChange("openAiApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>API Key</span>
|
||||
<span className="font-medium">API Key</span>
|
||||
</VSCodeTextField>
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId="gpt-4o"
|
||||
defaultModelInfo={openAiModelInfoSaneDefaults}
|
||||
models={openAiModels}
|
||||
modelIdKey="openAiModelId"
|
||||
modelInfoKey="openAiCustomModelInfo"
|
||||
serviceName="OpenAI"
|
||||
serviceUrl="https://platform.openai.com"
|
||||
recommendedModel="gpt-4-turbo-preview"
|
||||
models={openAiModels}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelInfo={openAiModelInfoSaneDefaults}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<Checkbox
|
||||
|
|
@ -749,12 +743,7 @@ const ApiOptions = ({
|
|||
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 15,
|
||||
}}
|
||||
/>
|
||||
<div className="mt-4" />
|
||||
<Pane
|
||||
title="Model Configuration"
|
||||
open={false}
|
||||
|
|
@ -814,7 +803,7 @@ const ApiOptions = ({
|
|||
}
|
||||
})}
|
||||
placeholder="e.g. 4096">
|
||||
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
|
||||
<span className="font-medium">Max Output Tokens</span>
|
||||
</VSCodeTextField>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -864,7 +853,7 @@ const ApiOptions = ({
|
|||
}
|
||||
})}
|
||||
placeholder="e.g. 128000">
|
||||
<span style={{ fontWeight: 500 }}>Context Window Size</span>
|
||||
<span className="font-medium">Context Window Size</span>
|
||||
</VSCodeTextField>
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -902,7 +891,7 @@ const ApiOptions = ({
|
|||
supportsImages: checked,
|
||||
}
|
||||
})}>
|
||||
<span style={{ fontWeight: 500 }}>Image Support</span>
|
||||
<span className="font-medium">Image Support</span>
|
||||
</Checkbox>
|
||||
<i
|
||||
className="codicon codicon-info"
|
||||
|
|
@ -946,7 +935,7 @@ const ApiOptions = ({
|
|||
supportsComputerUse: checked,
|
||||
}
|
||||
})}>
|
||||
<span style={{ fontWeight: 500 }}>Computer Use</span>
|
||||
<span className="font-medium">Computer Use</span>
|
||||
</Checkbox>
|
||||
<i
|
||||
className="codicon codicon-info"
|
||||
|
|
@ -1011,7 +1000,7 @@ const ApiOptions = ({
|
|||
})}
|
||||
placeholder="e.g. 0.0001">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<span style={{ fontWeight: 500 }}>Input Price</span>
|
||||
<span className="font-medium">Input Price</span>
|
||||
<i
|
||||
className="codicon codicon-info"
|
||||
title="Cost per million tokens in the input/prompt. This affects the cost of sending context and instructions to the model."
|
||||
|
|
@ -1056,7 +1045,7 @@ const ApiOptions = ({
|
|||
})}
|
||||
placeholder="e.g. 0.0002">
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<span style={{ fontWeight: 500 }}>Output Price</span>
|
||||
<span className="font-medium">Output Price</span>
|
||||
<i
|
||||
className="codicon codicon-info"
|
||||
title="Cost per million tokens in the model's response. This affects the cost of generated content and completions."
|
||||
|
|
@ -1091,17 +1080,15 @@ const ApiOptions = ({
|
|||
type="url"
|
||||
onInput={handleInputChange("lmStudioBaseUrl")}
|
||||
placeholder={"Default: http://localhost:1234"}>
|
||||
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
|
||||
<span className="font-medium">Base URL (optional)</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.lmStudioModelId || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("lmStudioModelId")}
|
||||
placeholder={"e.g. meta-llama-3.1-8b-instruct"}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
<span className="font-medium">Model ID</span>
|
||||
</VSCodeTextField>
|
||||
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
|
||||
|
||||
{lmStudioModels.length > 0 && (
|
||||
<VSCodeRadioGroup
|
||||
value={
|
||||
|
|
@ -1139,7 +1126,7 @@ const ApiOptions = ({
|
|||
</VSCodeLink>{" "}
|
||||
feature to use it with this extension.{" "}
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||
(<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
|
||||
(<span className="font-medium">Note:</span> Roo Code uses complex prompts and works best
|
||||
with Claude models. Less capable models may not work as expected.)
|
||||
</span>
|
||||
</p>
|
||||
|
|
@ -1154,7 +1141,7 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onInput={handleInputChange("deepSeekApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>DeepSeek API Key</span>
|
||||
<span className="font-medium">DeepSeek API Key</span>
|
||||
</VSCodeTextField>
|
||||
<p
|
||||
style={{
|
||||
|
|
@ -1178,7 +1165,7 @@ const ApiOptions = ({
|
|||
<div>
|
||||
<div className="dropdown-container">
|
||||
<label htmlFor="vscode-lm-model">
|
||||
<span style={{ fontWeight: 500 }}>Language Model</span>
|
||||
<span className="font-medium">Language Model</span>
|
||||
</label>
|
||||
{vsCodeLmModels.length > 0 ? (
|
||||
<Dropdown
|
||||
|
|
@ -1237,14 +1224,14 @@ const ApiOptions = ({
|
|||
type="url"
|
||||
onInput={handleInputChange("ollamaBaseUrl")}
|
||||
placeholder={"Default: http://localhost:11434"}>
|
||||
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
|
||||
<span className="font-medium">Base URL (optional)</span>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.ollamaModelId || ""}
|
||||
style={{ width: "100%" }}
|
||||
onInput={handleInputChange("ollamaModelId")}
|
||||
placeholder={"e.g. llama3.1"}>
|
||||
<span style={{ fontWeight: 500 }}>Model ID</span>
|
||||
<span className="font-medium">Model ID</span>
|
||||
</VSCodeTextField>
|
||||
{errorMessage && (
|
||||
<div className="text-vscode-errorForeground text-sm">
|
||||
|
|
@ -1284,7 +1271,7 @@ const ApiOptions = ({
|
|||
quickstart guide.
|
||||
</VSCodeLink>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||
(<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
|
||||
(<span className="font-medium">Note:</span> Roo Code uses complex prompts and works best
|
||||
with Claude models. Less capable models may not work as expected.)
|
||||
</span>
|
||||
</p>
|
||||
|
|
@ -1299,7 +1286,7 @@ const ApiOptions = ({
|
|||
type="password"
|
||||
onChange={handleInputChange("unboundApiKey")}
|
||||
placeholder="Enter API Key...">
|
||||
<span style={{ fontWeight: 500 }}>Unbound API Key</span>
|
||||
<span className="font-medium">Unbound API Key</span>
|
||||
</VSCodeTextField>
|
||||
{!apiConfiguration?.unboundApiKey && (
|
||||
<VSCodeButtonLink
|
||||
|
|
@ -1317,38 +1304,9 @@ const ApiOptions = ({
|
|||
}}>
|
||||
This key is stored locally and only used to make API requests from this extension.
|
||||
</p>
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
defaultModelId={unboundDefaultModelId}
|
||||
defaultModelInfo={unboundDefaultModelInfo}
|
||||
models={unboundModels}
|
||||
modelInfoKey="unboundModelInfo"
|
||||
modelIdKey="unboundModelId"
|
||||
serviceName="Unbound"
|
||||
serviceUrl="https://api.getunbound.ai/models"
|
||||
recommendedModel={unboundDefaultModelId}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedProvider === "glama" && (
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration ?? {}}
|
||||
defaultModelId={glamaDefaultModelId}
|
||||
defaultModelInfo={glamaDefaultModelInfo}
|
||||
models={glamaModels}
|
||||
modelInfoKey="glamaModelInfo"
|
||||
modelIdKey="glamaModelId"
|
||||
serviceName="Glama"
|
||||
serviceUrl="https://glama.ai/models"
|
||||
recommendedModel="anthropic/claude-3-7-sonnet"
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "openrouter" && (
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
|
|
@ -1360,10 +1318,37 @@ const ApiOptions = ({
|
|||
modelInfoKey="openRouterModelInfo"
|
||||
serviceName="OpenRouter"
|
||||
serviceUrl="https://openrouter.ai/models"
|
||||
recommendedModel="anthropic/claude-3.7-sonnet"
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "glama" && (
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId={glamaDefaultModelId}
|
||||
defaultModelInfo={glamaDefaultModelInfo}
|
||||
models={glamaModels}
|
||||
modelInfoKey="glamaModelInfo"
|
||||
modelIdKey="glamaModelId"
|
||||
serviceName="Glama"
|
||||
serviceUrl="https://glama.ai/models"
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "unbound" && (
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
defaultModelId={unboundDefaultModelId}
|
||||
defaultModelInfo={unboundDefaultModelInfo}
|
||||
models={unboundModels}
|
||||
modelInfoKey="unboundModelInfo"
|
||||
modelIdKey="unboundModelId"
|
||||
serviceName="Unbound"
|
||||
serviceUrl="https://api.getunbound.ai/models"
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "requesty" && (
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
|
|
@ -1375,58 +1360,37 @@ const ApiOptions = ({
|
|||
modelInfoKey="requestyModelInfo"
|
||||
serviceName="Requesty"
|
||||
serviceUrl="https://requesty.ai"
|
||||
recommendedModel="anthropic/claude-3-7-sonnet-latest"
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider !== "glama" &&
|
||||
selectedProvider !== "openrouter" &&
|
||||
selectedProvider !== "requesty" &&
|
||||
selectedProvider !== "openai" &&
|
||||
selectedProvider !== "ollama" &&
|
||||
selectedProvider !== "lmstudio" &&
|
||||
selectedProvider !== "unbound" && (
|
||||
<>
|
||||
<div className="dropdown-container">
|
||||
<label htmlFor="model-id">
|
||||
<span style={{ fontWeight: 500 }}>Model</span>
|
||||
</label>
|
||||
{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)}
|
||||
</div>
|
||||
{errorMessage && <ApiErrorMessage errorMessage={errorMessage} />}
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
isDescriptionExpanded={isDescriptionExpanded}
|
||||
setIsDescriptionExpanded={setIsDescriptionExpanded}
|
||||
{selectedProviderModelOptions.length > 0 && (
|
||||
<>
|
||||
<div className="dropdown-container">
|
||||
<label htmlFor="model-id" className="font-medium">
|
||||
Model
|
||||
</label>
|
||||
<Dropdown
|
||||
id="model-id"
|
||||
value={selectedModelId}
|
||||
onChange={(value) => {
|
||||
setApiConfigurationField("apiModelId", typeof value == "string" ? value : value?.value)
|
||||
}}
|
||||
options={selectedProviderModelOptions}
|
||||
className="w-full"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedModelInfo && selectedModelInfo.thinking && (
|
||||
<div className="flex flex-col gap-1 mt-2">
|
||||
<div className="font-medium">Thinking Budget</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Slider
|
||||
min={THINKING_BUDGET.min}
|
||||
max={(selectedModelInfo.maxTokens ?? THINKING_BUDGET.default) - 1}
|
||||
step={THINKING_BUDGET.step}
|
||||
value={[anthropicThinkingBudget]}
|
||||
onValueChange={(value) => setApiConfigurationField("anthropicThinking", value[0])}
|
||||
/>
|
||||
<div className="w-12 text-sm text-center">{anthropicThinkingBudget}</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
Number of tokens Claude is allowed to use for its internal reasoning process.
|
||||
</div>
|
||||
</div>
|
||||
<ThinkingBudget
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
modelInfo={selectedModelInfo}
|
||||
/>
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
isDescriptionExpanded={isDescriptionExpanded}
|
||||
setIsDescriptionExpanded={setIsDescriptionExpanded}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!fromWelcomeView && (
|
||||
|
|
@ -1459,6 +1423,7 @@ export function normalizeApiConfiguration(apiConfiguration?: ApiConfiguration) {
|
|||
const getProviderData = (models: Record<string, ModelInfo>, 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T> = NonNullable<
|
||||
{ [K in keyof ApiConfiguration]: Required<ApiConfiguration>[K] extends T ? K : never }[keyof ApiConfiguration]
|
||||
|
|
@ -14,24 +16,17 @@ type ExtractType<T> = 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<string, ModelInfo> | null
|
||||
modelIdKey: ModelIdKeys
|
||||
modelInfoKey: ExtractType<ModelInfo>
|
||||
serviceName: string
|
||||
serviceUrl: string
|
||||
recommendedModel: string
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: <K extends keyof ApiConfiguration>(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 (
|
||||
<>
|
||||
<div className="font-semibold">Model</div>
|
||||
<Combobox
|
||||
style={errorMessage ? { "--color-vscode-dropdown-border": "var(--color-vscode-errorForeground)" } : {}}
|
||||
type="single"
|
||||
inputValue={apiConfiguration[modelIdKey]}
|
||||
onInputValueChange={onSelect}>
|
||||
<ComboboxInput
|
||||
className="border-vscode-errorForeground tefat"
|
||||
placeholder="Search model..."
|
||||
data-testid="model-input"
|
||||
aria-errormessage={errorMessage}
|
||||
/>
|
||||
<Combobox type="single" inputValue={inputValue} onInputValueChange={onSelect}>
|
||||
<ComboboxInput placeholder="Search model..." data-testid="model-input" />
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No model found.</ComboboxEmpty>
|
||||
{modelIds.map((model) => (
|
||||
|
|
@ -92,31 +84,18 @@ export const ModelPicker = ({
|
|||
))}
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
|
||||
{errorMessage ? (
|
||||
<ApiErrorMessage errorMessage={errorMessage}>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: 3,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<span style={{ color: "var(--vscode-errorForeground)" }}>
|
||||
<span style={{ fontWeight: 500 }}>Note:</span> Roo Code uses complex prompts and works best
|
||||
with Claude models. Less capable models may not work as expected.
|
||||
</span>
|
||||
</p>
|
||||
</ApiErrorMessage>
|
||||
) : (
|
||||
selectedModelId &&
|
||||
selectedModelInfo && (
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
isDescriptionExpanded={isDescriptionExpanded}
|
||||
setIsDescriptionExpanded={setIsDescriptionExpanded}
|
||||
/>
|
||||
)
|
||||
<ThinkingBudget
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
modelInfo={selectedModelInfo}
|
||||
/>
|
||||
{selectedModelId && selectedModelInfo && selectedModelId === inputValue && (
|
||||
<ModelInfoView
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
isDescriptionExpanded={isDescriptionExpanded}
|
||||
setIsDescriptionExpanded={setIsDescriptionExpanded}
|
||||
/>
|
||||
)}
|
||||
<p>
|
||||
The extension automatically fetches the latest list of models available on{" "}
|
||||
|
|
@ -124,7 +103,7 @@ export const ModelPicker = ({
|
|||
{serviceName}.
|
||||
</VSCodeLink>
|
||||
If you're unsure which model to choose, Roo Code works best with{" "}
|
||||
<VSCodeLink onClick={() => onSelect(recommendedModel)}>{recommendedModel}.</VSCodeLink>
|
||||
<VSCodeLink onClick={() => onSelect(defaultModelId)}>{defaultModelId}.</VSCodeLink>
|
||||
You can also try searching "free" for no-cost options currently available.
|
||||
</p>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -66,21 +66,20 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
[isChangeDetected],
|
||||
)
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
checkUnsaveChanges,
|
||||
}),
|
||||
[checkUnsaveChanges],
|
||||
)
|
||||
useImperativeHandle(ref, () => ({ checkUnsaveChanges }), [checkUnsaveChanges])
|
||||
|
||||
const onConfirmDialogResult = useCallback((confirm: boolean) => {
|
||||
if (confirm) {
|
||||
|
|
@ -200,10 +193,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
const newCommands = [...currentCommands, commandInput]
|
||||
setCachedStateField("allowedCommands", newCommands)
|
||||
setCommandInput("")
|
||||
vscode.postMessage({
|
||||
type: "allowedCommands",
|
||||
commands: newCommands,
|
||||
})
|
||||
vscode.postMessage({ type: "allowedCommands", commands: newCommands })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
29
webview-ui/src/components/settings/ThinkingBudget.tsx
Normal file
29
webview-ui/src/components/settings/ThinkingBudget.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { Slider } from "@/components/ui"
|
||||
|
||||
import { ApiConfiguration, ModelInfo, THINKING_BUDGET } from "../../../../src/shared/api"
|
||||
|
||||
interface ThinkingBudgetProps {
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: <K extends keyof ApiConfiguration>(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 ? (
|
||||
<div className="flex flex-col gap-1 mt-2">
|
||||
<div className="font-medium">Thinking Budget</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Slider
|
||||
min={THINKING_BUDGET.min}
|
||||
max={(modelInfo.maxTokens ?? THINKING_BUDGET.default) - 1}
|
||||
step={THINKING_BUDGET.step}
|
||||
value={[budget]}
|
||||
onValueChange={(value) => setApiConfigurationField("anthropicThinking", value[0])}
|
||||
/>
|
||||
<div className="w-12 text-sm text-center">{budget}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
|
@ -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<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
}
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
function AlertDialogTrigger({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
function AlertDialogOverlay({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-vscode-editor-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-2 text-left", className)} {...props} />
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
function AlertDialogContent({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-row justify-end space-x-2", className)} {...props} />
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-base text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
|
||||
function AlertDialogTitle({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant: "secondary" }), "mt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
function AlertDialogAction({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogCancel({ className, ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return <AlertDialogPrimitive.Cancel className={cn(buttonVariants({ variant: "outline" }), className)} {...props} />
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
|
|
|
|||
|
|
@ -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<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-vscode-editor-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="cursor-pointer absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
function DialogContent({ className, children, ...props }: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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<string, ModelInfo>,
|
||||
openRouterModels?: Record<string, ModelInfo>,
|
||||
unboundModels?: Record<string, ModelInfo>,
|
||||
requestyModels?: Record<string, ModelInfo>,
|
||||
): 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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue