Organize provider settings into separate components (#3196)

This commit is contained in:
Chris Estreich 2025-05-05 15:53:01 -07:00 • committed by GitHub
parent 883be32b1a
commit da6c50bf10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1550 additions and 1183 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Organize provider settings into separate components

View file

@ -19,11 +19,11 @@ describe("ChutesHandler", () => {
beforeEach(() => {
jest.clearAllMocks()
mockCreate = (OpenAI as unknown as jest.Mock)().chat.completions.create
handler = new ChutesHandler({})
handler = new ChutesHandler({ chutesApiKey: "test-chutes-api-key" })
})
test("should use the correct Chutes base URL", () => {
new ChutesHandler({})
new ChutesHandler({ chutesApiKey: "test-chutes-api-key" })
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://llm.chutes.ai/v1" }))
})
@ -41,9 +41,8 @@ describe("ChutesHandler", () => {
test("should return specified model when valid model is provided", () => {
const testModelId: ChutesModelId = "deepseek-ai/DeepSeek-R1"
const handlerWithModel = new ChutesHandler({ apiModelId: testModelId })
const handlerWithModel = new ChutesHandler({ apiModelId: testModelId, chutesApiKey: "test-chutes-api-key" })
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(chutesModels[testModelId])
})
@ -110,7 +109,7 @@ describe("ChutesHandler", () => {
test("createMessage should pass correct parameters to Chutes client", async () => {
const modelId: ChutesModelId = "deepseek-ai/DeepSeek-R1"
const modelInfo = chutesModels[modelId]
const handlerWithModel = new ChutesHandler({ apiModelId: modelId })
const handlerWithModel = new ChutesHandler({ apiModelId: modelId, chutesApiKey: "test-chutes-api-key" })
mockCreate.mockImplementationOnce(() => {
return {

View file

@ -19,11 +19,11 @@ describe("GroqHandler", () => {
beforeEach(() => {
jest.clearAllMocks()
mockCreate = (OpenAI as unknown as jest.Mock)().chat.completions.create
handler = new GroqHandler({})
handler = new GroqHandler({ groqApiKey: "test-groq-api-key" })
})
test("should use the correct Groq base URL", () => {
new GroqHandler({})
new GroqHandler({ groqApiKey: "test-groq-api-key" })
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.groq.com/openai/v1" }))
})
@ -35,17 +35,16 @@ describe("GroqHandler", () => {
test("should return default model when no model is specified", () => {
const model = handler.getModel()
expect(model.id).toBe(groqDefaultModelId) // Use groqDefaultModelId
expect(model.info).toEqual(groqModels[groqDefaultModelId]) // Use groqModels
expect(model.id).toBe(groqDefaultModelId)
expect(model.info).toEqual(groqModels[groqDefaultModelId])
})
test("should return specified model when valid model is provided", () => {
const testModelId: GroqModelId = "llama-3.3-70b-versatile" // Use a valid Groq model ID and type
const handlerWithModel = new GroqHandler({ apiModelId: testModelId }) // Instantiate GroqHandler
const testModelId: GroqModelId = "llama-3.3-70b-versatile"
const handlerWithModel = new GroqHandler({ apiModelId: testModelId, groqApiKey: "test-groq-api-key" })
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(groqModels[testModelId]) // Use groqModels
expect(model.info).toEqual(groqModels[testModelId])
})
test("completePrompt method should return text from Groq API", async () => {
@ -110,7 +109,7 @@ describe("GroqHandler", () => {
test("createMessage should pass correct parameters to Groq client", async () => {
const modelId: GroqModelId = "llama-3.1-8b-instant"
const modelInfo = groqModels[modelId]
const handlerWithModel = new GroqHandler({ apiModelId: modelId })
const handlerWithModel = new GroqHandler({ apiModelId: modelId, groqApiKey: "test-groq-api-key" })
mockCreate.mockImplementationOnce(() => {
return {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,84 @@
import { useCallback, useState } from "react"
import { Checkbox } from "vscrui"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ApiConfiguration } from "@roo/shared/api"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform, noTransform } from "../transforms"
type AnthropicProps = {
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
}
export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: AnthropicProps) => {
const { t } = useAppTranslation()
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
const handleInputChange = useCallback(
<K extends keyof ApiConfiguration, E>(
field: K,
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
return (
<>
<VSCodeTextField
value={apiConfiguration?.apiKey || ""}
type="password"
onInput={handleInputChange("apiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.anthropicApiKey")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.apiKey && (
<VSCodeButtonLink href="https://console.anthropic.com/settings/keys" appearance="secondary">
{t("settings:providers.getAnthropicApiKey")}
</VSCodeButtonLink>
)}
<div>
<Checkbox
checked={anthropicBaseUrlSelected}
onChange={(checked: boolean) => {
setAnthropicBaseUrlSelected(checked)
if (!checked) {
setApiConfigurationField("anthropicBaseUrl", "")
setApiConfigurationField("anthropicUseAuthToken", false)
}
}}>
{t("settings:providers.useCustomBaseUrl")}
</Checkbox>
{anthropicBaseUrlSelected && (
<>
<VSCodeTextField
value={apiConfiguration?.anthropicBaseUrl || ""}
type="url"
onInput={handleInputChange("anthropicBaseUrl")}
placeholder="https://api.anthropic.com"
className="w-full mt-1"
/>
<Checkbox
checked={apiConfiguration?.anthropicUseAuthToken ?? false}
onChange={handleInputChange("anthropicUseAuthToken", noTransform)}
className="w-full mt-1">
{t("settings:providers.anthropicUseAuthToken")}
</Checkbox>
</>
)}
</div>
</>
)
}

View file

@ -0,0 +1,126 @@
import { useCallback } from "react"
import { Checkbox } from "vscrui"
import { VSCodeTextField, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview-ui-toolkit/react"
import { ApiConfiguration, ModelInfo } from "@roo/shared/api"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
import { AWS_REGIONS } from "../constants"
import { inputEventTransform, noTransform } from "../transforms"
type BedrockProps = {
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
selectedModelInfo?: ModelInfo
}
export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedModelInfo }: BedrockProps) => {
const { t } = useAppTranslation()
const handleInputChange = useCallback(
<K extends keyof ApiConfiguration, E>(
field: K,
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
return (
<>
<VSCodeRadioGroup
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
onChange={handleInputChange(
"awsUseProfile",
(e) => (e.target as HTMLInputElement).value === "profile",
)}>
<VSCodeRadio value="credentials">{t("settings:providers.awsCredentials")}</VSCodeRadio>
<VSCodeRadio value="profile">{t("settings:providers.awsProfile")}</VSCodeRadio>
</VSCodeRadioGroup>
<div className="text-sm text-vscode-descriptionForeground -mt-3">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{apiConfiguration?.awsUseProfile ? (
<VSCodeTextField
value={apiConfiguration?.awsProfile || ""}
onInput={handleInputChange("awsProfile")}
placeholder={t("settings:placeholders.profileName")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.awsProfileName")}</label>
</VSCodeTextField>
) : (
<>
<VSCodeTextField
value={apiConfiguration?.awsAccessKey || ""}
type="password"
onInput={handleInputChange("awsAccessKey")}
placeholder={t("settings:placeholders.accessKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.awsAccessKey")}</label>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSecretKey || ""}
type="password"
onInput={handleInputChange("awsSecretKey")}
placeholder={t("settings:placeholders.secretKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.awsSecretKey")}</label>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSessionToken || ""}
type="password"
onInput={handleInputChange("awsSessionToken")}
placeholder={t("settings:placeholders.sessionToken")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.awsSessionToken")}</label>
</VSCodeTextField>
</>
)}
<div>
<label className="block font-medium mb-1">{t("settings:providers.awsRegion")}</label>
<Select
value={apiConfiguration?.awsRegion || ""}
onValueChange={(value) => setApiConfigurationField("awsRegion", value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
{AWS_REGIONS.map(({ value, label }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Checkbox
checked={apiConfiguration?.awsUseCrossRegionInference || false}
onChange={handleInputChange("awsUseCrossRegionInference", noTransform)}>
{t("settings:providers.awsCrossRegion")}
</Checkbox>
{selectedModelInfo?.supportsPromptCache && (
<Checkbox
checked={apiConfiguration?.awsUsePromptCache || false}
onChange={handleInputChange("awsUsePromptCache", noTransform)}>
<div className="flex items-center gap-1">
<span>{t("settings:providers.enablePromptCaching")}</span>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.enablePromptCachingTitle")}
style={{ fontSize: "12px" }}
/>
</div>
</Checkbox>
)}
<div>
<div className="text-sm text-vscode-descriptionForeground ml-6 mt-1">
{t("settings:providers.cacheUsageNote")}
</div>
</div>
</>
)
}

View file

@ -0,0 +1,77 @@
import { useCallback, useState } from "react"
import { Checkbox } from "vscrui"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ApiConfiguration } from "@roo/shared/api"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
type GeminiProps = {
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
}
export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiProps) => {
const { t } = useAppTranslation()
const [googleGeminiBaseUrlSelected, setGoogleGeminiBaseUrlSelected] = useState(
!!apiConfiguration?.googleGeminiBaseUrl,
)
const handleInputChange = useCallback(
<K extends keyof ApiConfiguration, E>(
field: K,
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
return (
<>
<VSCodeTextField
value={apiConfiguration?.geminiApiKey || ""}
type="password"
onInput={handleInputChange("geminiApiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.geminiApiKey")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.geminiApiKey && (
<VSCodeButtonLink href="https://ai.google.dev/" appearance="secondary">
{t("settings:providers.getGeminiApiKey")}
</VSCodeButtonLink>
)}
<div>
<Checkbox
checked={googleGeminiBaseUrlSelected}
onChange={(checked: boolean) => {
setGoogleGeminiBaseUrlSelected(checked)
if (!checked) {
setApiConfigurationField("googleGeminiBaseUrl", "")
}
}}>
{t("settings:providers.useCustomBaseUrl")}
</Checkbox>
{googleGeminiBaseUrlSelected && (
<VSCodeTextField
value={apiConfiguration?.googleGeminiBaseUrl || ""}
type="url"
onInput={handleInputChange("googleGeminiBaseUrl")}
placeholder={t("settings:defaults.geminiUrl")}
className="w-full mt-1"
/>
)}
</div>
</>
)
}

View file

@ -0,0 +1,152 @@
import { useCallback, useState } from "react"
import { useEvent } from "react-use"
import { Trans } from "react-i18next"
import { Checkbox } from "vscrui"
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ApiConfiguration } from "@roo/shared/api"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
import { inputEventTransform } from "../transforms"
type LMStudioProps = {
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
}
export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudioProps) => {
const { t } = useAppTranslation()
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
const handleInputChange = useCallback(
<K extends keyof ApiConfiguration, E>(
field: K,
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
const onMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "lmStudioModels":
{
const newModels = message.lmStudioModels ?? []
setLmStudioModels(newModels)
}
break
}
}, [])
useEvent("message", onMessage)
return (
<>
<VSCodeTextField
value={apiConfiguration?.lmStudioBaseUrl || ""}
type="url"
onInput={handleInputChange("lmStudioBaseUrl")}
placeholder={t("settings:defaults.lmStudioUrl")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.lmStudio.baseUrl")}</label>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.lmStudioModelId || ""}
onInput={handleInputChange("lmStudioModelId")}
placeholder={t("settings:placeholders.modelId.lmStudio")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.lmStudio.modelId")}</label>
</VSCodeTextField>
{lmStudioModels.length > 0 && (
<VSCodeRadioGroup
value={
lmStudioModels.includes(apiConfiguration?.lmStudioModelId || "")
? apiConfiguration?.lmStudioModelId
: ""
}
onChange={handleInputChange("lmStudioModelId")}>
{lmStudioModels.map((model) => (
<VSCodeRadio key={model} value={model} checked={apiConfiguration?.lmStudioModelId === model}>
{model}
</VSCodeRadio>
))}
</VSCodeRadioGroup>
)}
<Checkbox
checked={apiConfiguration?.lmStudioSpeculativeDecodingEnabled === true}
onChange={(checked) => {
setApiConfigurationField("lmStudioSpeculativeDecodingEnabled", checked)
}}>
{t("settings:providers.lmStudio.speculativeDecoding")}
</Checkbox>
{apiConfiguration?.lmStudioSpeculativeDecodingEnabled && (
<>
<div>
<VSCodeTextField
value={apiConfiguration?.lmStudioDraftModelId || ""}
onInput={handleInputChange("lmStudioDraftModelId")}
placeholder={t("settings:placeholders.modelId.lmStudioDraft")}
className="w-full">
<label className="block font-medium mb-1">
{t("settings:providers.lmStudio.draftModelId")}
</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.lmStudio.draftModelDesc")}
</div>
</div>
{lmStudioModels.length > 0 && (
<>
<div className="font-medium">{t("settings:providers.lmStudio.selectDraftModel")}</div>
<VSCodeRadioGroup
value={
lmStudioModels.includes(apiConfiguration?.lmStudioDraftModelId || "")
? apiConfiguration?.lmStudioDraftModelId
: ""
}
onChange={handleInputChange("lmStudioDraftModelId")}>
{lmStudioModels.map((model) => (
<VSCodeRadio key={`draft-${model}`} value={model}>
{model}
</VSCodeRadio>
))}
</VSCodeRadioGroup>
{lmStudioModels.length === 0 && (
<div
className="text-sm rounded-xs p-2"
style={{
backgroundColor: "var(--vscode-inputValidation-infoBackground)",
border: "1px solid var(--vscode-inputValidation-infoBorder)",
color: "var(--vscode-inputValidation-infoForeground)",
}}>
{t("settings:providers.lmStudio.noModelsFound")}
</div>
)}
</>
)}
</>
)}
<div className="text-sm text-vscode-descriptionForeground">
<Trans
i18nKey="settings:providers.lmStudio.description"
components={{
a: <VSCodeLink href="https://lmstudio.ai/docs" />,
b: <VSCodeLink href="https://lmstudio.ai/docs/basics/server" />,
span: (
<span className="text-vscode-errorForeground ml-1">
<span className="font-medium">Note:</span>
</span>
),
}}
/>
</div>
</>
)
}

View file

@ -0,0 +1,86 @@
import { useState, useCallback } from "react"
import { useEvent } from "react-use"
import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
import { ApiConfiguration } from "@roo/shared/api"
import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { inputEventTransform } from "../transforms"
type OllamaProps = {
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
}
export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaProps) => {
const { t } = useAppTranslation()
const [ollamaModels, setOllamaModels] = useState<string[]>([])
const handleInputChange = useCallback(
<K extends keyof ApiConfiguration, E>(
field: K,
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
const onMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "ollamaModels":
{
const newModels = message.ollamaModels ?? []
setOllamaModels(newModels)
}
break
}
}, [])
useEvent("message", onMessage)
return (
<>
<VSCodeTextField
value={apiConfiguration?.ollamaBaseUrl || ""}
type="url"
onInput={handleInputChange("ollamaBaseUrl")}
placeholder={t("settings:defaults.ollamaUrl")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.ollama.baseUrl")}</label>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.ollamaModelId || ""}
onInput={handleInputChange("ollamaModelId")}
placeholder={t("settings:placeholders.modelId.ollama")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.ollama.modelId")}</label>
</VSCodeTextField>
{ollamaModels.length > 0 && (
<VSCodeRadioGroup
value={
ollamaModels.includes(apiConfiguration?.ollamaModelId || "")
? apiConfiguration?.ollamaModelId
: ""
}
onChange={handleInputChange("ollamaModelId")}>
{ollamaModels.map((model) => (
<VSCodeRadio key={model} value={model} checked={apiConfiguration?.ollamaModelId === model}>
{model}
</VSCodeRadio>
))}
</VSCodeRadioGroup>
)}
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.ollama.description")}
<span className="text-vscode-errorForeground ml-1">{t("settings:providers.ollama.warning")}</span>
</div>
</>
)
}

View file

@ -0,0 +1,77 @@
import { useCallback, useState } from "react"
import { Checkbox } from "vscrui"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ApiConfiguration } from "@roo/shared/api"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
type OpenAIProps = {
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
}
export const OpenAI = ({ apiConfiguration, setApiConfigurationField }: OpenAIProps) => {
const { t } = useAppTranslation()
const [openAiNativeBaseUrlSelected, setOpenAiNativeBaseUrlSelected] = useState(
!!apiConfiguration?.openAiNativeBaseUrl,
)
const handleInputChange = useCallback(
<K extends keyof ApiConfiguration, E>(
field: K,
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
return (
<>
<Checkbox
checked={openAiNativeBaseUrlSelected}
onChange={(checked: boolean) => {
setOpenAiNativeBaseUrlSelected(checked)
if (!checked) {
setApiConfigurationField("openAiNativeBaseUrl", "")
}
}}>
{t("settings:providers.useCustomBaseUrl")}
</Checkbox>
{openAiNativeBaseUrlSelected && (
<>
<VSCodeTextField
value={apiConfiguration?.openAiNativeBaseUrl || ""}
type="url"
onInput={handleInputChange("openAiNativeBaseUrl")}
placeholder="https://api.openai.com/v1"
className="w-full mt-1"
/>
</>
)}
<VSCodeTextField
value={apiConfiguration?.openAiNativeApiKey || ""}
type="password"
onInput={handleInputChange("openAiNativeApiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.openAiApiKey")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.openAiNativeApiKey && (
<VSCodeButtonLink href="https://platform.openai.com/api-keys" appearance="secondary">
{t("settings:providers.getOpenAiApiKey")}
</VSCodeButtonLink>
)}
</>
)
}

View file

@ -0,0 +1,578 @@
import { useState, useCallback } from "react"
import { useEvent } from "react-use"
import { Checkbox } from "vscrui"
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ModelInfo, ReasoningEffort as ReasoningEffortType } from "@roo/schemas"
import { ApiConfiguration, azureOpenAiDefaultApiVersion, openAiModelInfoSaneDefaults } from "@roo/shared/api"
import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Button } from "@src/components/ui"
import { inputEventTransform, noTransform } from "../transforms"
import { ModelPicker } from "../ModelPicker"
import { R1FormatSetting } from "../R1FormatSetting"
import { ReasoningEffort } from "../ReasoningEffort"
type OpenAICompatibleProps = {
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
}
export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }: OpenAICompatibleProps) => {
const { t } = useAppTranslation()
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
const [openAiLegacyFormatSelected, setOpenAiLegacyFormatSelected] = useState(!!apiConfiguration?.openAiLegacyFormat)
const [openAiModels, setOpenAiModels] = useState<Record<string, ModelInfo> | null>(null)
const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
const headers = apiConfiguration?.openAiHeaders || {}
return Object.entries(headers)
})
const handleAddCustomHeader = useCallback(() => {
// Only update the local state to show the new row in the UI.
setCustomHeaders((prev) => [...prev, ["", ""]])
// Do not update the main configuration yet, wait for user input.
}, [])
const handleUpdateHeaderKey = useCallback((index: number, newKey: string) => {
setCustomHeaders((prev) => {
const updated = [...prev]
if (updated[index]) {
updated[index] = [newKey, updated[index][1]]
}
return updated
})
}, [])
const handleUpdateHeaderValue = useCallback((index: number, newValue: string) => {
setCustomHeaders((prev) => {
const updated = [...prev]
if (updated[index]) {
updated[index] = [updated[index][0], newValue]
}
return updated
})
}, [])
const handleRemoveCustomHeader = useCallback((index: number) => {
setCustomHeaders((prev) => prev.filter((_, i) => i !== index))
}, [])
const handleInputChange = useCallback(
<K extends keyof ApiConfiguration, E>(
field: K,
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
const onMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "openAiModels": {
const updatedModels = message.openAiModels ?? []
setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
break
}
}
}, [])
useEvent("message", onMessage)
return (
<>
<VSCodeTextField
value={apiConfiguration?.openAiBaseUrl || ""}
type="url"
onInput={handleInputChange("openAiBaseUrl")}
placeholder={t("settings:placeholders.baseUrl")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.openAiBaseUrl")}</label>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.openAiApiKey || ""}
type="password"
onInput={handleInputChange("openAiApiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.openAiApiKey")}</label>
</VSCodeTextField>
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId="gpt-4o"
models={openAiModels}
modelIdKey="openAiModelId"
serviceName="OpenAI"
serviceUrl="https://platform.openai.com"
/>
<R1FormatSetting
onChange={handleInputChange("openAiR1FormatEnabled", noTransform)}
openAiR1FormatEnabled={apiConfiguration?.openAiR1FormatEnabled ?? false}
/>
<div>
<Checkbox
checked={openAiLegacyFormatSelected}
onChange={(checked: boolean) => {
setOpenAiLegacyFormatSelected(checked)
setApiConfigurationField("openAiLegacyFormat", checked)
}}>
{t("settings:providers.useLegacyFormat")}
</Checkbox>
</div>
<Checkbox
checked={apiConfiguration?.openAiStreamingEnabled ?? true}
onChange={handleInputChange("openAiStreamingEnabled", noTransform)}>
{t("settings:modelInfo.enableStreaming")}
</Checkbox>
<Checkbox
checked={apiConfiguration?.openAiUseAzure ?? false}
onChange={handleInputChange("openAiUseAzure", noTransform)}>
{t("settings:modelInfo.useAzure")}
</Checkbox>
<div>
<Checkbox
checked={azureApiVersionSelected}
onChange={(checked: boolean) => {
setAzureApiVersionSelected(checked)
if (!checked) {
setApiConfigurationField("azureApiVersion", "")
}
}}>
{t("settings:modelInfo.azureApiVersion")}
</Checkbox>
{azureApiVersionSelected && (
<VSCodeTextField
value={apiConfiguration?.azureApiVersion || ""}
onInput={handleInputChange("azureApiVersion")}
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
className="w-full mt-1"
/>
)}
</div>
{/* Custom Headers UI */}
<div className="mb-4">
<div className="flex justify-between items-center mb-2">
<label className="block font-medium">{t("settings:providers.customHeaders")}</label>
<VSCodeButton appearance="icon" title={t("settings:common.add")} onClick={handleAddCustomHeader}>
<span className="codicon codicon-add"></span>
</VSCodeButton>
</div>
{!customHeaders.length ? (
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.noCustomHeaders")}
</div>
) : (
customHeaders.map(([key, value], index) => (
<div key={index} className="flex items-center mb-2">
<VSCodeTextField
value={key}
className="flex-1 mr-2"
placeholder={t("settings:providers.headerName")}
onInput={(e: any) => handleUpdateHeaderKey(index, e.target.value)}
/>
<VSCodeTextField
value={value}
className="flex-1 mr-2"
placeholder={t("settings:providers.headerValue")}
onInput={(e: any) => handleUpdateHeaderValue(index, e.target.value)}
/>
<VSCodeButton
appearance="icon"
title={t("settings:common.remove")}
onClick={() => handleRemoveCustomHeader(index)}>
<span className="codicon codicon-trash"></span>
</VSCodeButton>
</div>
))
)}
</div>
<div className="flex flex-col gap-1">
<Checkbox
checked={apiConfiguration.enableReasoningEffort ?? false}
onChange={(checked: boolean) => {
setApiConfigurationField("enableReasoningEffort", checked)
if (!checked) {
const { reasoningEffort: _, ...openAiCustomModelInfo } =
apiConfiguration.openAiCustomModelInfo || openAiModelInfoSaneDefaults
setApiConfigurationField("openAiCustomModelInfo", openAiCustomModelInfo)
}
}}>
{t("settings:providers.setReasoningLevel")}
</Checkbox>
{!!apiConfiguration.enableReasoningEffort && (
<ReasoningEffort
apiConfiguration={{
...apiConfiguration,
reasoningEffort: apiConfiguration.openAiCustomModelInfo?.reasoningEffort,
}}
setApiConfigurationField={(field, value) => {
if (field === "reasoningEffort") {
const openAiCustomModelInfo =
apiConfiguration.openAiCustomModelInfo || openAiModelInfoSaneDefaults
setApiConfigurationField("openAiCustomModelInfo", {
...openAiCustomModelInfo,
reasoningEffort: value as ReasoningEffortType,
})
}
}}
/>
)}
</div>
<div className="flex flex-col gap-3">
<div className="text-sm text-vscode-descriptionForeground whitespace-pre-line">
{t("settings:providers.customModel.capabilities")}
</div>
<div>
<VSCodeTextField
value={
apiConfiguration?.openAiCustomModelInfo?.maxTokens?.toString() ||
openAiModelInfoSaneDefaults.maxTokens?.toString() ||
""
}
type="text"
style={{
borderColor: (() => {
const value = apiConfiguration?.openAiCustomModelInfo?.maxTokens
if (!value) {
return "var(--vscode-input-border)"
}
return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
})(),
}}
title={t("settings:providers.customModel.maxTokens.description")}
onInput={handleInputChange("openAiCustomModelInfo", (e) => {
const value = parseInt((e.target as HTMLInputElement).value)
return {
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
maxTokens: isNaN(value) ? undefined : value,
}
})}
placeholder={t("settings:placeholders.numbers.maxTokens")}
className="w-full">
<label className="block font-medium mb-1">
{t("settings:providers.customModel.maxTokens.label")}
</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.customModel.maxTokens.description")}
</div>
</div>
<div>
<VSCodeTextField
value={
apiConfiguration?.openAiCustomModelInfo?.contextWindow?.toString() ||
openAiModelInfoSaneDefaults.contextWindow?.toString() ||
""
}
type="text"
style={{
borderColor: (() => {
const value = apiConfiguration?.openAiCustomModelInfo?.contextWindow
if (!value) {
return "var(--vscode-input-border)"
}
return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
})(),
}}
title={t("settings:providers.customModel.contextWindow.description")}
onInput={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
const parsed = parseInt(value)
return {
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
contextWindow: isNaN(parsed) ? openAiModelInfoSaneDefaults.contextWindow : parsed,
}
})}
placeholder={t("settings:placeholders.numbers.contextWindow")}
className="w-full">
<label className="block font-medium mb-1">
{t("settings:providers.customModel.contextWindow.label")}
</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.customModel.contextWindow.description")}
</div>
</div>
<div>
<div className="flex items-center gap-1">
<Checkbox
checked={
apiConfiguration?.openAiCustomModelInfo?.supportsImages ??
openAiModelInfoSaneDefaults.supportsImages
}
onChange={handleInputChange("openAiCustomModelInfo", (checked) => {
return {
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
supportsImages: checked,
}
})}>
<span className="font-medium">
{t("settings:providers.customModel.imageSupport.label")}
</span>
</Checkbox>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.imageSupport.description")}
style={{ fontSize: "12px" }}
/>
</div>
<div className="text-sm text-vscode-descriptionForeground pt-1">
{t("settings:providers.customModel.imageSupport.description")}
</div>
</div>
<div>
<div className="flex items-center gap-1">
<Checkbox
checked={apiConfiguration?.openAiCustomModelInfo?.supportsComputerUse ?? false}
onChange={handleInputChange("openAiCustomModelInfo", (checked) => {
return {
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
supportsComputerUse: checked,
}
})}>
<span className="font-medium">{t("settings:providers.customModel.computerUse.label")}</span>
</Checkbox>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.computerUse.description")}
style={{ fontSize: "12px" }}
/>
</div>
<div className="text-sm text-vscode-descriptionForeground pt-1">
{t("settings:providers.customModel.computerUse.description")}
</div>
</div>
<div>
<div className="flex items-center gap-1">
<Checkbox
checked={apiConfiguration?.openAiCustomModelInfo?.supportsPromptCache ?? false}
onChange={handleInputChange("openAiCustomModelInfo", (checked) => {
return {
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
supportsPromptCache: checked,
}
})}>
<span className="font-medium">{t("settings:providers.customModel.promptCache.label")}</span>
</Checkbox>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.promptCache.description")}
style={{ fontSize: "12px" }}
/>
</div>
<div className="text-sm text-vscode-descriptionForeground pt-1">
{t("settings:providers.customModel.promptCache.description")}
</div>
</div>
<div>
<VSCodeTextField
value={
apiConfiguration?.openAiCustomModelInfo?.inputPrice?.toString() ??
openAiModelInfoSaneDefaults.inputPrice?.toString() ??
""
}
type="text"
style={{
borderColor: (() => {
const value = apiConfiguration?.openAiCustomModelInfo?.inputPrice
if (!value && value !== 0) {
return "var(--vscode-input-border)"
}
return value >= 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
})(),
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
const parsed = parseFloat(value)
return {
...(apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults),
inputPrice: isNaN(parsed) ? openAiModelInfoSaneDefaults.inputPrice : parsed,
}
})}
placeholder={t("settings:placeholders.numbers.inputPrice")}
className="w-full">
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">
{t("settings:providers.customModel.pricing.input.label")}
</label>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.pricing.input.description")}
style={{ fontSize: "12px" }}
/>
</div>
</VSCodeTextField>
</div>
<div>
<VSCodeTextField
value={
apiConfiguration?.openAiCustomModelInfo?.outputPrice?.toString() ||
openAiModelInfoSaneDefaults.outputPrice?.toString() ||
""
}
type="text"
style={{
borderColor: (() => {
const value = apiConfiguration?.openAiCustomModelInfo?.outputPrice
if (!value && value !== 0) {
return "var(--vscode-input-border)"
}
return value >= 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
})(),
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
const parsed = parseFloat(value)
return {
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
outputPrice: isNaN(parsed) ? openAiModelInfoSaneDefaults.outputPrice : parsed,
}
})}
placeholder={t("settings:placeholders.numbers.outputPrice")}
className="w-full">
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">
{t("settings:providers.customModel.pricing.output.label")}
</label>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.pricing.output.description")}
style={{ fontSize: "12px" }}
/>
</div>
</VSCodeTextField>
</div>
{apiConfiguration?.openAiCustomModelInfo?.supportsPromptCache && (
<>
<div>
<VSCodeTextField
value={apiConfiguration?.openAiCustomModelInfo?.cacheReadsPrice?.toString() ?? "0"}
type="text"
style={{
borderColor: (() => {
const value = apiConfiguration?.openAiCustomModelInfo?.cacheReadsPrice
if (!value && value !== 0) {
return "var(--vscode-input-border)"
}
return value >= 0
? "var(--vscode-charts-green)"
: "var(--vscode-errorForeground)"
})(),
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
const parsed = parseFloat(value)
return {
...(apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults),
cacheReadsPrice: isNaN(parsed) ? 0 : parsed,
}
})}
placeholder={t("settings:placeholders.numbers.inputPrice")}
className="w-full">
<div className="flex items-center gap-1">
<span className="font-medium">
{t("settings:providers.customModel.pricing.cacheReads.label")}
</span>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.pricing.cacheReads.description")}
style={{ fontSize: "12px" }}
/>
</div>
</VSCodeTextField>
</div>
<div>
<VSCodeTextField
value={apiConfiguration?.openAiCustomModelInfo?.cacheWritesPrice?.toString() ?? "0"}
type="text"
style={{
borderColor: (() => {
const value = apiConfiguration?.openAiCustomModelInfo?.cacheWritesPrice
if (!value && value !== 0) {
return "var(--vscode-input-border)"
}
return value >= 0
? "var(--vscode-charts-green)"
: "var(--vscode-errorForeground)"
})(),
}}
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
const parsed = parseFloat(value)
return {
...(apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults),
cacheWritesPrice: isNaN(parsed) ? 0 : parsed,
}
})}
placeholder={t("settings:placeholders.numbers.cacheWritePrice")}
className="w-full">
<div className="flex items-center gap-1">
<label className="block font-medium mb-1">
{t("settings:providers.customModel.pricing.cacheWrites.label")}
</label>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.pricing.cacheWrites.description")}
style={{ fontSize: "12px" }}
/>
</div>
</VSCodeTextField>
</div>
</>
)}
<Button
variant="secondary"
onClick={() => setApiConfigurationField("openAiCustomModelInfo", openAiModelInfoSaneDefaults)}>
{t("settings:providers.customModel.resetDefaults")}
</Button>
</div>
</>
)
}

View file

@ -0,0 +1,109 @@
import { useCallback, useState } from "react"
import { Trans } from "react-i18next"
import { Checkbox } from "vscrui"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ApiConfiguration } from "@roo/shared/api"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { getOpenRouterAuthUrl } from "@src/oauth/urls"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform, noTransform } from "../transforms"
import { OpenRouterBalanceDisplay } from "./OpenRouterBalanceDisplay"
type OpenRouterProps = {
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
uriScheme: string | undefined
fromWelcomeView?: boolean
}
export const OpenRouter = ({
apiConfiguration,
setApiConfigurationField,
uriScheme,
fromWelcomeView,
}: OpenRouterProps) => {
const { t } = useAppTranslation()
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
const handleInputChange = useCallback(
<K extends keyof ApiConfiguration, E>(
field: K,
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
return (
<>
<VSCodeTextField
value={apiConfiguration?.openRouterApiKey || ""}
type="password"
onInput={handleInputChange("openRouterApiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
<div className="flex justify-between items-center mb-1">
<label className="block font-medium">{t("settings:providers.openRouterApiKey")}</label>
{apiConfiguration?.openRouterApiKey && (
<OpenRouterBalanceDisplay
apiKey={apiConfiguration.openRouterApiKey}
baseUrl={apiConfiguration.openRouterBaseUrl}
/>
)}
</div>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink href={getOpenRouterAuthUrl(uriScheme)} style={{ width: "100%" }} appearance="primary">
{t("settings:providers.getOpenRouterApiKey")}
</VSCodeButtonLink>
)}
{!fromWelcomeView && (
<>
<div>
<Checkbox
checked={openRouterBaseUrlSelected}
onChange={(checked: boolean) => {
setOpenRouterBaseUrlSelected(checked)
if (!checked) {
setApiConfigurationField("openRouterBaseUrl", "")
}
}}>
{t("settings:providers.useCustomBaseUrl")}
</Checkbox>
{openRouterBaseUrlSelected && (
<VSCodeTextField
value={apiConfiguration?.openRouterBaseUrl || ""}
type="url"
onInput={handleInputChange("openRouterBaseUrl")}
placeholder="Default: https://openrouter.ai/api/v1"
className="w-full mt-1"
/>
)}
</div>
<Checkbox
checked={apiConfiguration?.openRouterUseMiddleOutTransform ?? true}
onChange={handleInputChange("openRouterUseMiddleOutTransform", noTransform)}>
<Trans
i18nKey="settings:providers.openRouterTransformsText"
components={{
// eslint-disable-next-line jsx-a11y/anchor-has-content
a: <a href="https://openrouter.ai/docs/transforms" />,
}}
/>
</Checkbox>
</>
)}
</>
)
}

View file

@ -0,0 +1,86 @@
import { useState, useCallback } from "react"
import { useEvent } from "react-use"
import { LanguageModelChatSelector } from "vscode"
import { ApiConfiguration } from "@roo/shared/api"
import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
import { inputEventTransform } from "../transforms"
type VSCodeLMProps = {
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
}
export const VSCodeLM = ({ apiConfiguration, setApiConfigurationField }: VSCodeLMProps) => {
const { t } = useAppTranslation()
const [vsCodeLmModels, setVsCodeLmModels] = useState<LanguageModelChatSelector[]>([])
const handleInputChange = useCallback(
<K extends keyof ApiConfiguration, E>(
field: K,
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
const onMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "vsCodeLmModels":
{
const newModels = message.vsCodeLmModels ?? []
setVsCodeLmModels(newModels)
}
break
}
}, [])
useEvent("message", onMessage)
return (
<>
<div>
<label className="block font-medium mb-1">{t("settings:providers.vscodeLmModel")}</label>
{vsCodeLmModels.length > 0 ? (
<Select
value={
apiConfiguration?.vsCodeLmModelSelector
? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}`
: ""
}
onValueChange={handleInputChange("vsCodeLmModelSelector", (value) => {
const [vendor, family] = value.split("/")
return { vendor, family }
})}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
{vsCodeLmModels.map((model) => (
<SelectItem
key={`${model.vendor}/${model.family}`}
value={`${model.vendor}/${model.family}`}>
{`${model.vendor} - ${model.family}`}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.vscodeLmDescription")}
</div>
)}
</div>
<div className="text-sm text-vscode-errorForeground">{t("settings:providers.vscodeLmWarning")}</div>
</>
)
}

View file

@ -0,0 +1,97 @@
import { useCallback } from "react"
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ApiConfiguration } from "@roo/shared/api"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
import { inputEventTransform } from "../transforms"
import { VERTEX_REGIONS } from "../constants"
type VertexProps = {
apiConfiguration: ApiConfiguration
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
}
export const Vertex = ({ apiConfiguration, setApiConfigurationField }: VertexProps) => {
const { t } = useAppTranslation()
const handleInputChange = useCallback(
<K extends keyof ApiConfiguration, E>(
field: K,
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
return (
<>
<div className="text-sm text-vscode-descriptionForeground">
<div>{t("settings:providers.googleCloudSetup.title")}</div>
<div>
<VSCodeLink
href="https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#before_you_begin"
className="text-sm">
{t("settings:providers.googleCloudSetup.step1")}
</VSCodeLink>
</div>
<div>
<VSCodeLink
href="https://cloud.google.com/docs/authentication/provide-credentials-adc#google-idp"
className="text-sm">
{t("settings:providers.googleCloudSetup.step2")}
</VSCodeLink>
</div>
<div>
<VSCodeLink
href="https://developers.google.com/workspace/guides/create-credentials?hl=en#service-account"
className="text-sm">
{t("settings:providers.googleCloudSetup.step3")}
</VSCodeLink>
</div>
</div>
<VSCodeTextField
value={apiConfiguration?.vertexJsonCredentials || ""}
onInput={handleInputChange("vertexJsonCredentials")}
placeholder={t("settings:placeholders.credentialsJson")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.googleCloudCredentials")}</label>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.vertexKeyFile || ""}
onInput={handleInputChange("vertexKeyFile")}
placeholder={t("settings:placeholders.keyFilePath")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.googleCloudKeyFile")}</label>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.vertexProjectId || ""}
onInput={handleInputChange("vertexProjectId")}
placeholder={t("settings:placeholders.projectId")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.googleCloudProjectId")}</label>
</VSCodeTextField>
<div>
<label className="block font-medium mb-1">{t("settings:providers.googleCloudRegion")}</label>
<Select
value={apiConfiguration?.vertexRegion || ""}
onValueChange={(value) => setApiConfigurationField("vertexRegion", value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
{VERTEX_REGIONS.map(({ value, label }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</>
)
}

View file

@ -0,0 +1,3 @@
export const noTransform = <T>(value: T) => value
export const inputEventTransform = <E>(event: E) => (event as { target: HTMLInputElement })?.target?.value as any